diff options
author | Andrew Arnott <andrewarnott@gmail.com> | 2010-02-02 12:08:23 -0800 |
---|---|---|
committer | Andrew Arnott <andrewarnott@gmail.com> | 2010-02-02 12:08:23 -0800 |
commit | 86d8fac0ff736090377966fe7e8e6bd4dbab9ddc (patch) | |
tree | a64ff0ee3890ec744ab539c57e86aaa182667eb7 | |
parent | 186f90aec15788c3790f84990752997b888cea65 (diff) | |
download | DotNetOpenAuth-86d8fac0ff736090377966fe7e8e6bd4dbab9ddc.zip DotNetOpenAuth-86d8fac0ff736090377966fe7e8e6bd4dbab9ddc.tar.gz DotNetOpenAuth-86d8fac0ff736090377966fe7e8e6bd4dbab9ddc.tar.bz2 |
Project template generation now ensures project item filenames that are short enough to not be rejected by VS Gallery or otherwise cause problems running up against MAX_PATH.
-rw-r--r-- | build.proj | 36 | ||||
-rw-r--r-- | lib/DotNetOpenAuth.BuildTasks.dll | bin | 67072 -> 71680 bytes | |||
-rw-r--r-- | lib/DotNetOpenAuth.BuildTasks.pdb | bin | 167424 -> 161280 bytes | |||
-rw-r--r-- | src/DotNetOpenAuth.BuildTasks/DotNetOpenAuth.BuildTasks.csproj | 1 | ||||
-rw-r--r-- | src/DotNetOpenAuth.BuildTasks/MergeProjectWithVSTemplate.cs | 134 | ||||
-rw-r--r-- | src/DotNetOpenAuth.BuildTasks/Utilities.cs | 31 |
6 files changed, 187 insertions, 15 deletions
@@ -158,7 +158,7 @@ <MSBuild Projects="@(ProjectTemplates)" /> </Target> - <Target Name="ProjectTemplatesLayout" DependsOnTargets="GetBuildVersion;BuildUnifiedProduct;ReSignDelaySignedAssemblies;BuildProjectTemplates"> + <Target Name="ProjectTemplatesLayout" DependsOnTargets="GetBuildVersion;BuildUnifiedProduct;ReSignDelaySignedAssemblies;BuildProjectTemplates;VsixManifestLayout"> <ItemGroup> <ProjectTemplatesSource Include="$(ProjectRoot)projecttemplates\**\*" Exclude=" @@ -228,11 +228,6 @@ <CopyWithTokenSubstitution SourceFiles="@(ProjectTemplatesTransformSource)" DestinationFiles="@(ProjectTemplatesTransformLayout)"> <Output TaskParameter="CopiedFiles" ItemName="CopiedProjectTemplateFiles" /> </CopyWithTokenSubstitution> - <Purge Directories="$(ProjectTemplatesLayoutPath)" - IntendedFiles=" - @(ProjectTemplatesLayout); - @(ProjectTemplatesTransformLayout); - " /> <ChangeProjectReferenceToAssemblyReference Projects="@(CopiedProjectTemplateFiles)" Condition="'%(Extension)' == '.csproj'" @@ -252,7 +247,23 @@ ProjectItemTypes="@(VsTemplateProjectItemTypes)" ReplaceParametersExtensions="@(VsTemplateParameterReplaceExtensions)" Templates="@(VSProjectTemplates)" - /> + VsixManifest="@(ProjectTemplates2010TransformLayout)" + EnsureMaxPath="240" + Condition=" '%(ProjectTemplates2010TransformLayout.Extension)' == '.vsixmanifest' " + > + <Output TaskParameter="MaxPathAdjustedPaths" ItemName="ProjectItemShortPathAdjustments"/> + </MergeProjectWithVSTemplate> + + <ItemGroup> + <ProjectTemplateIntendedFiles Include=" + @(ProjectTemplatesLayout); + @(ProjectTemplatesTransformLayout); + %(ProjectItemShortPathAdjustments.ShortPath); + " /> + <ProjectTemplateIntendedFiles Remove="@(ProjectItemShortPathAdjustments)" /> + </ItemGroup> + <Purge Directories="$(ProjectTemplatesLayoutPath)" + IntendedFiles="@(ProjectTemplateIntendedFiles)" /> </Target> <Target Name="ProjectTemplates2008" DependsOnTargets="ProjectTemplatesLayout"> @@ -394,7 +405,7 @@ /> </Target> - <Target Name="VsixLayout" DependsOnTargets="ProjectTemplates2010"> + <Target Name="VsixManifestLayout"> <ItemGroup> <ProjectTemplates2010TransformSource Include=" $(ProjectRoot)vsix\extension.vsixmanifest; @@ -405,7 +416,12 @@ <SkipUnchangedFiles>false</SkipUnchangedFiles> </ProjectTemplates2010TransformSource> <ProjectTemplates2010TransformLayout Include="@(ProjectTemplates2010TransformSource->'$(ExtensionVsixLayoutDirectory)%(RecursiveDir)%(FileName)%(Extension)')" /> - + </ItemGroup> + <CopyWithTokenSubstitution SourceFiles="@(ProjectTemplates2010TransformSource)" DestinationFiles="@(ProjectTemplates2010TransformLayout)" /> + </Target> + + <Target Name="VsixLayout" DependsOnTargets="ProjectTemplates2010;VsixManifestLayout"> + <ItemGroup> <ExtensionVsixSources Include=" $(ProjectRoot)vsix\*; " Exclude=" @@ -423,8 +439,6 @@ </ItemGroup> <Copy SourceFiles="@(ExtensionVsixSources)" DestinationFiles="@(ExtensionVsixTargets)" SkipUnchangedFiles="true" /> - <CopyWithTokenSubstitution SourceFiles="@(ProjectTemplates2010TransformSource)" DestinationFiles="@(ProjectTemplates2010TransformLayout)" /> - <Purge Directories="$(ExtensionVsixLayoutDirectory)" IntendedFiles="@(ExtensionVsixContents)" /> </Target> diff --git a/lib/DotNetOpenAuth.BuildTasks.dll b/lib/DotNetOpenAuth.BuildTasks.dll Binary files differindex 8edc333..7882acc 100644 --- a/lib/DotNetOpenAuth.BuildTasks.dll +++ b/lib/DotNetOpenAuth.BuildTasks.dll diff --git a/lib/DotNetOpenAuth.BuildTasks.pdb b/lib/DotNetOpenAuth.BuildTasks.pdb Binary files differindex e9ed7e3..88025fc 100644 --- a/lib/DotNetOpenAuth.BuildTasks.pdb +++ b/lib/DotNetOpenAuth.BuildTasks.pdb diff --git a/src/DotNetOpenAuth.BuildTasks/DotNetOpenAuth.BuildTasks.csproj b/src/DotNetOpenAuth.BuildTasks/DotNetOpenAuth.BuildTasks.csproj index 3f76f78..f107e81 100644 --- a/src/DotNetOpenAuth.BuildTasks/DotNetOpenAuth.BuildTasks.csproj +++ b/src/DotNetOpenAuth.BuildTasks/DotNetOpenAuth.BuildTasks.csproj @@ -131,6 +131,7 @@ <DependentUpon>TaskStrings.resx</DependentUpon> </Compile> <Compile Include="Trim.cs" /> + <Compile Include="Utilities.cs" /> </ItemGroup> <ItemGroup> <EmbeddedResource Include="TaskStrings.resx"> diff --git a/src/DotNetOpenAuth.BuildTasks/MergeProjectWithVSTemplate.cs b/src/DotNetOpenAuth.BuildTasks/MergeProjectWithVSTemplate.cs index 1a8a17d..c24d634 100644 --- a/src/DotNetOpenAuth.BuildTasks/MergeProjectWithVSTemplate.cs +++ b/src/DotNetOpenAuth.BuildTasks/MergeProjectWithVSTemplate.cs @@ -7,17 +7,25 @@ namespace DotNetOpenAuth.BuildTasks { using System; using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.IO; using System.Linq; using System.Text; - using Microsoft.Build.Framework; - using Microsoft.Build.Utilities; using System.Xml.Linq; - using System.IO; using Microsoft.Build.BuildEngine; - using System.Diagnostics.Contracts; + 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"; + + /// <summary> + /// A dictionary where the key is the project name and the value is the path contribution. + /// </summary> + private Dictionary<string, string> vsixContributionToPath = new Dictionary<string,string>(StringComparer.OrdinalIgnoreCase); [Required] public string[] ProjectItemTypes { get; set; } @@ -29,9 +37,36 @@ namespace DotNetOpenAuth.BuildTasks { public ITaskItem[] Templates { get; set; } /// <summary> + /// Gets or sets the path to the .vsixmanifest file that will be used to assist + /// in calculating the actual full path of project items. + /// </summary> + /// <remarks> + /// This property is required if <see cref="EnsureMaxPath"/> > 0; + /// </remarks> + public ITaskItem VsixManifest { get; set; } + + /// <summary> + /// Gets or sets the maximum length a project item's relative path should + /// be limited to, artificially renaming them as necessary. + /// </summary> + /// <value>Use 0 to disable the renaming feature.</value> + public int EnsureMaxPath { get; set; } + + /// <summary> + /// Gets or sets the project items that had to be renamed to comply with maximum path length requirements. + /// </summary> + /// <remarks> + /// The item name is the original full path. The ShortPath metadata contains the shortened full path. + /// </remarks> + [Output] + public ITaskItem[] MaxPathAdjustedPaths { get; set; } + + /// <summary> /// Executes this instance. /// </summary> public override bool Execute() { + var shortenedItems = new List<ITaskItem>(); + foreach(ITaskItem sourceTemplateTaskItem in this.Templates) { var template = XElement.Load(sourceTemplateTaskItem.ItemSpec); var templateContentElement = template.Element(XName.Get("TemplateContent", VSTemplateNamespace)); @@ -42,6 +77,7 @@ namespace DotNetOpenAuth.BuildTasks { } 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); @@ -66,15 +102,74 @@ namespace DotNetOpenAuth.BuildTasks { 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<ArgumentException>(!string.IsNullOrEmpty(fileName)); + Contract.Ensures(!string.IsNullOrEmpty(Contract.Result<string>())); + + // 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<ArgumentNullException>(projectElement != null); @@ -99,5 +194,36 @@ namespace DotNetOpenAuth.BuildTasks { return parent; } + + private string EstimateFullPathInProjectCache(string projectName, string itemRelativePath) { + Contract.Requires<ArgumentException>(!string.IsNullOrEmpty(projectName)); + Contract.Requires<ArgumentException>(!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); + } } } diff --git a/src/DotNetOpenAuth.BuildTasks/Utilities.cs b/src/DotNetOpenAuth.BuildTasks/Utilities.cs new file mode 100644 index 0000000..80e1733 --- /dev/null +++ b/src/DotNetOpenAuth.BuildTasks/Utilities.cs @@ -0,0 +1,31 @@ +//----------------------------------------------------------------------- +// <copyright file="Utilities.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.BuildTasks { + using System; + using System.Collections.Generic; + using System.Diagnostics.Contracts; + using System.Linq; + using System.Text; + + internal static class Utilities { + internal static string SuppressCharacters(string fileName, char[] suppress, char replacement) { + Contract.Requires<ArgumentNullException>(fileName != null); + Contract.Requires<ArgumentNullException>(suppress != null); + + if (fileName.IndexOfAny(suppress) < 0) { + return fileName; + } + + StringBuilder builder = new StringBuilder(fileName); + foreach (char ch in suppress) { + builder.Replace(ch, replacement); + } + + return builder.ToString(); + } + } +} |