//-----------------------------------------------------------------------
//
// Copyright (c) Andrew Arnott. All rights reserved.
//
//-----------------------------------------------------------------------
namespace DotNetOpenAuth.BuildTasks {
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using Microsoft.Build.BuildEngine;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using System.Globalization;
public class MergeProjectWithVSTemplate : Task {
internal const string VSTemplateNamespace = "http://schemas.microsoft.com/developer/vstemplate/2005";
internal const string VsixNamespace = "http://schemas.microsoft.com/developer/vsx-schema/2010";
///
/// A dictionary where the key is the project name and the value is the path contribution.
///
private Dictionary vsixContributionToPath = new Dictionary(StringComparer.OrdinalIgnoreCase);
[Required]
public string[] ProjectItemTypes { get; set; }
[Required]
public string[] ReplaceParametersExtensions { get; set; }
[Required]
public ITaskItem[] Templates { get; set; }
///
/// Gets or sets the path to the .vsixmanifest file that will be used to assist
/// in calculating the actual full path of project items.
///
///
/// This property is required if > 0;
///
public ITaskItem VsixManifest { get; set; }
///
/// Gets or sets the maximum length a project item's relative path should
/// be limited to, artificially renaming them as necessary.
///
/// Use 0 to disable the renaming feature.
public int EnsureMaxPath { get; set; }
///
/// Gets or sets the project items that had to be renamed to comply with maximum path length requirements.
///
///
/// The item name is the original full path. The ShortPath metadata contains the shortened full path.
///
[Output]
public ITaskItem[] MaxPathAdjustedPaths { get; set; }
///
/// Executes this instance.
///
public override bool Execute() {
var shortenedItems = new List();
foreach(ITaskItem sourceTemplateTaskItem in this.Templates) {
var template = XElement.Load(sourceTemplateTaskItem.ItemSpec);
var templateContentElement = template.Element(XName.Get("TemplateContent", VSTemplateNamespace));
var projectElement = templateContentElement.Element(XName.Get("Project", VSTemplateNamespace));
if (projectElement == null) {
Log.LogMessage("Skipping merge of \"{0}\" with a project because no project was referenced from the template.", sourceTemplateTaskItem.ItemSpec);
continue;
}
var projectPath = Path.Combine(Path.GetDirectoryName(sourceTemplateTaskItem.ItemSpec), projectElement.Attribute("File").Value);
var projectDirectory = Path.GetDirectoryName(Path.Combine(Path.GetDirectoryName(sourceTemplateTaskItem.GetMetadata("FullPath")), projectElement.Attribute("File").Value));
Log.LogMessage("Merging project \"{0}\" with \"{1}\".", projectPath, sourceTemplateTaskItem.ItemSpec);
var sourceProject = new Project();
sourceProject.Load(projectPath);
// Collect the project items from the project that are appropriate
// to include in the .vstemplate file.
var itemsByFolder = from item in sourceProject.EvaluatedItems.Cast()
where this.ProjectItemTypes.Contains(item.Name)
orderby item.Include
group item by Path.GetDirectoryName(item.Include);
foreach (var folder in itemsByFolder) {
XElement parentNode = FindOrCreateParent(folder.Key, projectElement);
foreach (var item in folder) {
bool replaceParameters = this.ReplaceParametersExtensions.Contains(Path.GetExtension(item.Include));
var itemName = XName.Get("ProjectItem", VSTemplateNamespace);
var projectItem = parentNode.Elements(itemName).FirstOrDefault(el => string.Equals(el.Value, Path.GetFileName(item.Include), StringComparison.OrdinalIgnoreCase));
if (projectItem == null) {
projectItem = new XElement(itemName, Path.GetFileName(item.Include));
parentNode.Add(projectItem);
}
if (replaceParameters) {
projectItem.SetAttributeValue("ReplaceParameters", "true");
}
if (this.EnsureMaxPath > 0) {
string estimatedFullPath = EstimateFullPathInProjectCache(Path.GetFileNameWithoutExtension(sourceProject.FullFileName), item.Include);
if (estimatedFullPath.Length > this.EnsureMaxPath) {
string leafName = Path.GetFileName(item.Include);
int targetLeafLength = leafName.Length - (estimatedFullPath.Length - this.EnsureMaxPath);
string shortenedFileName = CreateUniqueShortFileName(leafName, targetLeafLength);
string shortenedRelativePath = Path.Combine(Path.GetDirectoryName(item.Include), shortenedFileName);
string shortenedEstimatedFullPath = EstimateFullPathInProjectCache(Path.GetFileNameWithoutExtension(sourceProject.FullFileName), shortenedRelativePath);
if (shortenedEstimatedFullPath.Length <= this.EnsureMaxPath) {
this.Log.LogMessage(
"Renaming long project item '{0}' to '{1}' within project template to avoid MAX_PATH issues. The instantiated project will remain unchanged.",
item.Include,
shortenedRelativePath);
projectItem.SetAttributeValue("TargetFileName", Path.GetFileName(item.Include));
projectItem.Value = shortenedFileName;
string originalFullPath = Path.Combine(projectDirectory, item.Include);
string shortenedFullPath = Path.Combine(projectDirectory, shortenedRelativePath);
if (File.Exists(shortenedFullPath)) {
File.Delete(shortenedFullPath); // File.Move can't overwrite files
}
File.Move(originalFullPath, shortenedFullPath);
// Document the change so the build system can account for it.
TaskItem shortChange = new TaskItem(originalFullPath);
shortChange.SetMetadata("ShortPath", shortenedFullPath);
shortenedItems.Add(shortChange);
} else {
this.Log.LogError(
"Project item '{0}' exceeds maximum allowable length {1} by {2} characters and it cannot be sufficiently shortened. Estimated full path is: '{3}'.",
item.Include,
this.EnsureMaxPath,
estimatedFullPath.Length - this.EnsureMaxPath,
estimatedFullPath);
}
}
}
}
}
template.Save(sourceTemplateTaskItem.ItemSpec);
}
this.MaxPathAdjustedPaths = shortenedItems.ToArray();
return !Log.HasLoggedErrors;
}
private static string CreateUniqueShortFileName(string fileName, int targetLength) {
Contract.Requires(!string.IsNullOrEmpty(fileName));
Contract.Ensures(!string.IsNullOrEmpty(Contract.Result()));
// The filename may already full within the target length.
if (fileName.Length <= targetLength) {
return fileName;
}
string hashSuffix = Utilities.SuppressCharacters(fileName.GetHashCode().ToString("x"), Path.GetInvalidFileNameChars(), '_');
string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(fileName);
string extension = Path.GetExtension(fileName);
string hashSuffixWithExtension = hashSuffix + extension;
// If the target length is itself shorter than the hash code, then we won't meet our goal,
// but at least put the hash in there so it's unique, and we'll return a string that's too long.
string shortenedFileName = fileName.Substring(0, Math.Max(0, targetLength - hashSuffixWithExtension.Length)) + hashSuffixWithExtension;
return shortenedFileName;
}
private static XElement FindOrCreateParent(string directoryName, XElement projectElement) {
Contract.Requires(projectElement != null);
if (string.IsNullOrEmpty(directoryName)) {
return projectElement;
}
string[] segments = directoryName.Split(Path.DirectorySeparatorChar);
XElement parent = projectElement;
for (int i = 0; i < segments.Length; i++) {
var candidateName = XName.Get("Folder", VSTemplateNamespace);
var candidate = parent.Elements(XName.Get("Folder", VSTemplateNamespace)).FirstOrDefault(n => n.Attribute("Name").Value == segments[i]);
if (candidate == null) {
candidate = new XElement(
candidateName,
new XAttribute("Name", segments[i]));
parent.Add(candidate);
}
parent = candidate;
}
return parent;
}
private string EstimateFullPathInProjectCache(string projectName, string itemRelativePath) {
Contract.Requires(!string.IsNullOrEmpty(projectName));
Contract.Requires(!string.IsNullOrEmpty(itemRelativePath));
const string PathRoot = @"c:\documents and settings\usernameZZZ\AppData\Local\Microsoft\VisualStudio\10.0\Extensions\";
string subPath;
if (!vsixContributionToPath.TryGetValue(projectName, out subPath)) {
if (this.VsixManifest == null) {
this.Log.LogError("The task parameter VsixManifest is required but missing.");
}
var vsixDocument = XDocument.Load(this.VsixManifest.ItemSpec);
XElement vsix = vsixDocument.Element(XName.Get("Vsix", VsixNamespace));
XElement identifier = vsix.Element(XName.Get("Identifier", VsixNamespace));
XElement content = vsix.Element(XName.Get("Content", VsixNamespace));
string author = identifier.Element(XName.Get("Author", VsixNamespace)).Value;
string name = identifier.Element(XName.Get("Name", VsixNamespace)).Value;
string version = identifier.Element(XName.Get("Version", VsixNamespace)).Value;
string pt = content.Element(XName.Get("ProjectTemplate", VsixNamespace)).Value;
vsixContributionToPath[projectName] = subPath = string.Format(
CultureInfo.InvariantCulture,
@"{0}\{1}\{2}\~PC\{3}\CSharp\Web\{4}.zip\",
author,
name,
version,
pt,
projectName
);
}
return Path.Combine(PathRoot + subPath + Path.GetFileNameWithoutExtension(projectName), itemRelativePath);
}
}
}