diff options
Diffstat (limited to 'src/DotNetOpenAuth.BuildTasks')
11 files changed, 443 insertions, 34 deletions
diff --git a/src/DotNetOpenAuth.BuildTasks/CopyWithTokenSubstitution.cs b/src/DotNetOpenAuth.BuildTasks/CopyWithTokenSubstitution.cs index e17d8f2..38f3b50 100644 --- a/src/DotNetOpenAuth.BuildTasks/CopyWithTokenSubstitution.cs +++ b/src/DotNetOpenAuth.BuildTasks/CopyWithTokenSubstitution.cs @@ -58,36 +58,47 @@ namespace DotNetOpenAuth.BuildTasks { string destPath = this.DestinationFiles[i].ItemSpec; bool skipUnchangedFiles = bool.Parse(this.SourceFiles[i].GetMetadata("SkipUnchangedFiles")); - // We deliberably consider newer destination files to be up-to-date rather than - // requiring equality because this task modifies the destination file while copying. - if (skipUnchangedFiles && File.GetLastWriteTimeUtc(sourcePath) < File.GetLastWriteTimeUtc(destPath)) { - Log.LogMessage(MessageImportance.Low, "Skipping \"{0}\" -> \"{1}\" because the destination is up to date.", sourcePath, destPath); - continue; - } + if (string.IsNullOrEmpty(this.SourceFiles[i].GetMetadata("BeforeTokens"))) { + // this is just a standard copy without token substitution + if (skipUnchangedFiles && File.GetLastWriteTimeUtc(sourcePath) == File.GetLastWriteTimeUtc(destPath)) { + Log.LogMessage(MessageImportance.Low, "Skipping \"{0}\" -> \"{1}\" because the destination is up to date.", sourcePath, destPath); + continue; + } - Log.LogMessage(MessageImportance.Normal, "Transforming \"{0}\" -> \"{1}\"", sourcePath, destPath); + Log.LogMessage(MessageImportance.Normal, "Copying \"{0}\" -> \"{1}\"", sourcePath, destPath); + File.Copy(sourcePath, destPath, true); + } else { + // We deliberably consider newer destination files to be up-to-date rather than + // requiring equality because this task modifies the destination file while copying. + if (skipUnchangedFiles && File.GetLastWriteTimeUtc(sourcePath) < File.GetLastWriteTimeUtc(destPath)) { + Log.LogMessage(MessageImportance.Low, "Skipping \"{0}\" -> \"{1}\" because the destination is up to date.", sourcePath, destPath); + continue; + } - string[] beforeTokens = this.SourceFiles[i].GetMetadata("BeforeTokens").Split(';'); - string[] afterTokens = this.SourceFiles[i].GetMetadata("AfterTokens").Split(';'); - if (beforeTokens.Length != afterTokens.Length) { - Log.LogError("Unequal number of before and after tokens. Before: \"{0}\". After \"{1}\".", beforeTokens, afterTokens); - return false; - } + Log.LogMessage(MessageImportance.Normal, "Transforming \"{0}\" -> \"{1}\"", sourcePath, destPath); - using (StreamReader sr = File.OpenText(sourcePath)) { - if (!Directory.Exists(Path.GetDirectoryName(destPath))) { - Directory.CreateDirectory(Path.GetDirectoryName(destPath)); + string[] beforeTokens = this.SourceFiles[i].GetMetadata("BeforeTokens").Split(';'); + string[] afterTokens = this.SourceFiles[i].GetMetadata("AfterTokens").Split(';'); + if (beforeTokens.Length != afterTokens.Length) { + Log.LogError("Unequal number of before and after tokens. Before: \"{0}\". After \"{1}\".", beforeTokens, afterTokens); + return false; } - using (StreamWriter sw = File.CreateText(destPath)) { - StringBuilder line = new StringBuilder(); - while (!sr.EndOfStream) { - line.Length = 0; - line.Append(sr.ReadLine()); - for (int j = 0; j < beforeTokens.Length; j++) { - line.Replace(beforeTokens[j], afterTokens[j]); - } - sw.WriteLine(line); + using (StreamReader sr = File.OpenText(sourcePath)) { + if (!Directory.Exists(Path.GetDirectoryName(destPath))) { + Directory.CreateDirectory(Path.GetDirectoryName(destPath)); + } + using (StreamWriter sw = File.CreateText(destPath)) { + StringBuilder line = new StringBuilder(); + while (!sr.EndOfStream) { + line.Length = 0; + line.Append(sr.ReadLine()); + for (int j = 0; j < beforeTokens.Length; j++) { + line.Replace(beforeTokens[j], afterTokens[j]); + } + + sw.WriteLine(line); + } } } } diff --git a/src/DotNetOpenAuth.BuildTasks/DiscoverProjectTemplates.cs b/src/DotNetOpenAuth.BuildTasks/DiscoverProjectTemplates.cs index 0162c16..f49c9b1 100644 --- a/src/DotNetOpenAuth.BuildTasks/DiscoverProjectTemplates.cs +++ b/src/DotNetOpenAuth.BuildTasks/DiscoverProjectTemplates.cs @@ -15,6 +15,7 @@ namespace DotNetOpenAuth.BuildTasks { using Microsoft.Build.Utilities; public class DiscoverProjectTemplates : Task { + [Required] public ITaskItem[] TopLevelTemplates { get; set; } [Output] diff --git a/src/DotNetOpenAuth.BuildTasks/DotNetOpenAuth.BuildTasks.csproj b/src/DotNetOpenAuth.BuildTasks/DotNetOpenAuth.BuildTasks.csproj index b8cff51..f107e81 100644 --- a/src/DotNetOpenAuth.BuildTasks/DotNetOpenAuth.BuildTasks.csproj +++ b/src/DotNetOpenAuth.BuildTasks/DotNetOpenAuth.BuildTasks.csproj @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="utf-8"?> -<Project ToolsVersion="3.5" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> +<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> @@ -12,6 +12,21 @@ <AssemblyName>DotNetOpenAuth.BuildTasks</AssemblyName> <TargetFrameworkVersion>v3.5</TargetFrameworkVersion> <FileAlignment>512</FileAlignment> + <PublishUrl>publish\</PublishUrl> + <Install>true</Install> + <InstallFrom>Disk</InstallFrom> + <UpdateEnabled>false</UpdateEnabled> + <UpdateMode>Foreground</UpdateMode> + <UpdateInterval>7</UpdateInterval> + <UpdateIntervalUnits>Days</UpdateIntervalUnits> + <UpdatePeriodically>false</UpdatePeriodically> + <UpdateRequired>false</UpdateRequired> + <MapFileExtensions>true</MapFileExtensions> + <ApplicationRevision>0</ApplicationRevision> + <ApplicationVersion>1.0.0.%2a</ApplicationVersion> + <IsWebBootstrapper>false</IsWebBootstrapper> + <UseApplicationTrust>false</UseApplicationTrust> + <BootstrapperEnabled>true</BootstrapperEnabled> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <DebugSymbols>true</DebugSymbols> @@ -48,6 +63,7 @@ </CodeContractsBaseLineFile> <CodeContractsRuntimeCheckingLevel>Full</CodeContractsRuntimeCheckingLevel> <CodeContractsReferenceAssembly>%28none%29</CodeContractsReferenceAssembly> + <CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <DebugType>pdbonly</DebugType> @@ -56,6 +72,7 @@ <DefineConstants>TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> + <CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet> </PropertyGroup> <ItemGroup> <Reference Include="Microsoft.Build.Engine" /> @@ -89,14 +106,17 @@ <Compile Include="CreateWebApplication.cs" /> <Compile Include="DeleteWebApplication.cs" /> <Compile Include="DiscoverProjectTemplates.cs" /> + <Compile Include="DowngradeProjects.cs" /> <Compile Include="ECMAScriptPacker.cs" /> <Compile Include="FilterItems.cs" /> <Compile Include="FixupReferenceHintPaths.cs" /> <Compile Include="FixupShippingToolSamples.cs" /> + <Compile Include="HardLinkCopy.cs" /> <Compile Include="MergeProjectWithVSTemplate.cs" /> <Compile Include="GetBuildVersion.cs" /> <Compile Include="CheckAdminRights.cs" /> <Compile Include="JsPack.cs" /> + <Compile Include="NativeMethods.cs" /> <Compile Include="ParseMaster.cs" /> <Compile Include="Publicize.cs" /> <Compile Include="Purge.cs" /> @@ -111,6 +131,7 @@ <DependentUpon>TaskStrings.resx</DependentUpon> </Compile> <Compile Include="Trim.cs" /> + <Compile Include="Utilities.cs" /> </ItemGroup> <ItemGroup> <EmbeddedResource Include="TaskStrings.resx"> @@ -118,6 +139,23 @@ <LastGenOutput>TaskStrings.Designer.cs</LastGenOutput> </EmbeddedResource> </ItemGroup> + <ItemGroup> + <BootstrapperPackage Include="Microsoft.Net.Client.3.5"> + <Visible>False</Visible> + <ProductName>.NET Framework 3.5 SP1 Client Profile</ProductName> + <Install>false</Install> + </BootstrapperPackage> + <BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1"> + <Visible>False</Visible> + <ProductName>.NET Framework 3.5 SP1</ProductName> + <Install>true</Install> + </BootstrapperPackage> + <BootstrapperPackage Include="Microsoft.Windows.Installer.3.1"> + <Visible>False</Visible> + <ProductName>Windows Installer 3.1</ProductName> + <Install>true</Install> + </BootstrapperPackage> + </ItemGroup> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <!-- To modify your build process, add your task inside one of the targets below and uncomment it. Other similar extension points exist, see Microsoft.Common.targets. diff --git a/src/DotNetOpenAuth.BuildTasks/DotNetOpenAuth.BuildTasks.sln b/src/DotNetOpenAuth.BuildTasks/DotNetOpenAuth.BuildTasks.sln index 0d0900b..34a8e46 100644 --- a/src/DotNetOpenAuth.BuildTasks/DotNetOpenAuth.BuildTasks.sln +++ b/src/DotNetOpenAuth.BuildTasks/DotNetOpenAuth.BuildTasks.sln @@ -1,8 +1,6 @@ -Microsoft Visual Studio Solution File, Format Version 10.00 -# Visual Studio 2008 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetOpenAuth.BuildTasks", "DotNetOpenAuth.BuildTasks.csproj", "{AC231A51-EF60-437C-A33F-AF8ADEB8EB74}" -EndProject +Microsoft Visual Studio Solution File, Format Version 11.00 +# Visual Studio 2010 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{ABBE14A3-0404-4123-9093-E598C3DD3E9B}" ProjectSection(SolutionItems) = preProject ..\..\build.proj = ..\..\build.proj @@ -13,6 +11,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ..\..\tools\DotNetOpenAuth.Versioning.targets = ..\..\tools\DotNetOpenAuth.Versioning.targets EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetOpenAuth.BuildTasks", "DotNetOpenAuth.BuildTasks.csproj", "{AC231A51-EF60-437C-A33F-AF8ADEB8EB74}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/src/DotNetOpenAuth.BuildTasks/DowngradeProjects.cs b/src/DotNetOpenAuth.BuildTasks/DowngradeProjects.cs new file mode 100644 index 0000000..645522d --- /dev/null +++ b/src/DotNetOpenAuth.BuildTasks/DowngradeProjects.cs @@ -0,0 +1,110 @@ +//----------------------------------------------------------------------- +// <copyright file="DowngradeProjects.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.BuildTasks { + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Text; + using Microsoft.Build.BuildEngine; + using Microsoft.Build.Framework; + using Microsoft.Build.Utilities; + + /// <summary> + /// Downgrades Visual Studio 2010 solutions and projects so that they load in Visual Studio 2008. + /// </summary> + public class DowngradeProjects : Task { + /// <summary> + /// Gets or sets the projects and solutions to downgrade. + /// </summary> + [Required] + public ITaskItem[] Projects { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether ASP.NET MVC 2 projects are downgraded to MVC 1.0. + /// </summary> + public bool DowngradeMvc2ToMvc1 { get; set; } + + /// <summary> + /// Executes this instance. + /// </summary> + public override bool Execute() { + foreach (ITaskItem taskItem in this.Projects) { + switch (GetClassification(taskItem)) { + case ProjectClassification.VS2010Project: + this.Log.LogMessage(MessageImportance.Low, "Downgrading project \"{0}\".", taskItem.ItemSpec); + Project project = new Project(); + project.Load(taskItem.ItemSpec); + project.DefaultToolsVersion = "3.5"; + + if (this.DowngradeMvc2ToMvc1) { + string projectTypeGuids = project.GetEvaluatedProperty("ProjectTypeGuids"); + if (!string.IsNullOrEmpty(projectTypeGuids)) { + projectTypeGuids = projectTypeGuids.Replace("{F85E285D-A4E0-4152-9332-AB1D724D3325}", "{603c0e0b-db56-11dc-be95-000d561079b0}"); + project.SetProperty("ProjectTypeGuids", projectTypeGuids); + } + } + + // Web projects usually have an import that includes these substrings + foreach (Import import in project.Imports) { + import.ProjectPath = import.ProjectPath + .Replace("$(MSBuildExtensionsPath32)", "$(MSBuildExtensionsPath)") + .Replace("VisualStudio\\v10.0", "VisualStudio\\v9.0"); + } + + // VS2010 won't let you have a System.Core reference, but VS2008 requires it. + BuildItemGroup references = project.GetEvaluatedItemsByName("Reference"); + if (!references.Cast<BuildItem>().Any(item => item.FinalItemSpec.StartsWith("System.Core", StringComparison.OrdinalIgnoreCase))) { + project.AddNewItem("Reference", "System.Core"); + } + + project.Save(taskItem.ItemSpec); + break; + case ProjectClassification.VS2010Solution: + this.Log.LogMessage(MessageImportance.Low, "Downgrading solution \"{0}\".", taskItem.ItemSpec); + string[] contents = File.ReadAllLines(taskItem.ItemSpec); + if (contents[1] != "Microsoft Visual Studio Solution File, Format Version 11.00" || + contents[2] != "# Visual Studio 2010") { + this.Log.LogError("Unrecognized solution file header in \"{0}\".", taskItem.ItemSpec); + break; + } + + contents[1] = "Microsoft Visual Studio Solution File, Format Version 10.00"; + contents[2] = "# Visual Studio 2008"; + + for (int i = 3; i < contents.Length; i++) { + contents[i] = contents[i].Replace("TargetFrameworkMoniker = \".NETFramework,Version%3Dv", "TargetFramework = \""); + } + + File.WriteAllLines(taskItem.ItemSpec, contents); + break; + default: + this.Log.LogWarning("Unrecognized project type for \"{0}\".", taskItem.ItemSpec); + break; + } + } + + return !this.Log.HasLoggedErrors; + } + + private static ProjectClassification GetClassification(ITaskItem taskItem) { + if (Path.GetExtension(taskItem.ItemSpec).EndsWith("proj", StringComparison.OrdinalIgnoreCase)) { + return ProjectClassification.VS2010Project; + } else if (Path.GetExtension(taskItem.ItemSpec).Equals(".sln", StringComparison.OrdinalIgnoreCase)) { + return ProjectClassification.VS2010Solution; + } else { + return ProjectClassification.Unrecognized; + } + } + + private enum ProjectClassification { + VS2010Project, + VS2010Solution, + Unrecognized, + } + } +} diff --git a/src/DotNetOpenAuth.BuildTasks/FixupShippingToolSamples.cs b/src/DotNetOpenAuth.BuildTasks/FixupShippingToolSamples.cs index a6088c9..6c71740 100644 --- a/src/DotNetOpenAuth.BuildTasks/FixupShippingToolSamples.cs +++ b/src/DotNetOpenAuth.BuildTasks/FixupShippingToolSamples.cs @@ -6,6 +6,7 @@ namespace DotNetOpenAuth.BuildTasks { using System; + using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; @@ -24,6 +25,8 @@ namespace DotNetOpenAuth.BuildTasks { public string[] RemoveImportsStartingWith { get; set; } + public ITaskItem[] AddReferences { get; set; } + /// <summary> /// Executes this instance. /// </summary> @@ -43,6 +46,15 @@ namespace DotNetOpenAuth.BuildTasks { .ForEach(import => project.Imports.RemoveImport(import)); } + if (this.AddReferences != null) { + foreach (var reference in this.AddReferences) { + BuildItem item = project.AddNewItem("Reference", reference.ItemSpec); + foreach (DictionaryEntry metadata in reference.CloneCustomMetadata()) { + item.SetMetadata((string)metadata.Key, (string)metadata.Value); + } + } + } + project.Save(projectTaskItem.ItemSpec); } diff --git a/src/DotNetOpenAuth.BuildTasks/GetBuildVersion.cs b/src/DotNetOpenAuth.BuildTasks/GetBuildVersion.cs index 23db9a6..1d60ca4 100644 --- a/src/DotNetOpenAuth.BuildTasks/GetBuildVersion.cs +++ b/src/DotNetOpenAuth.BuildTasks/GetBuildVersion.cs @@ -61,6 +61,7 @@ namespace DotNetOpenAuth.BuildTasks { string cmdPath = Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.System), "cmd.exe"); ProcessStartInfo psi = new ProcessStartInfo(cmdPath, "/c git rev-parse HEAD"); psi.WindowStyle = ProcessWindowStyle.Hidden; + psi.CreateNoWindow = true; psi.RedirectStandardOutput = true; psi.UseShellExecute = false; Process git = Process.Start(psi); diff --git a/src/DotNetOpenAuth.BuildTasks/HardLinkCopy.cs b/src/DotNetOpenAuth.BuildTasks/HardLinkCopy.cs new file mode 100644 index 0000000..af2d1d0 --- /dev/null +++ b/src/DotNetOpenAuth.BuildTasks/HardLinkCopy.cs @@ -0,0 +1,61 @@ +//----------------------------------------------------------------------- +// <copyright file="HardLinkCopy.cs" company="Andrew Arnott"> +// Copyright (c) Andrew Arnott. All rights reserved. +// </copyright> +//----------------------------------------------------------------------- + +namespace DotNetOpenAuth.BuildTasks { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using Microsoft.Build.Framework; + using Microsoft.Build.Utilities; + using System.IO; + + public class HardLinkCopy : Task { + [Required] + public ITaskItem[] SourceFiles { get; set; } + + [Required] + public ITaskItem[] DestinationFiles { get; set; } + + /// <summary> + /// Executes this instance. + /// </summary> + public override bool Execute() { + if (this.SourceFiles.Length != this.DestinationFiles.Length) { + this.Log.LogError("SourceFiles has {0} elements and DestinationFiles has {1} elements.", this.SourceFiles.Length, this.DestinationFiles.Length); + return false; + } + + for (int i = 0; i < this.SourceFiles.Length; i++) { + bool hardLink; + bool.TryParse(this.DestinationFiles[i].GetMetadata("HardLink"), out hardLink); + string sourceFile = this.SourceFiles[i].ItemSpec; + string destinationFile = this.DestinationFiles[i].ItemSpec; + this.Log.LogMessage( + MessageImportance.Low, + "Copying {0} -> {1}{2}.", + sourceFile, + destinationFile, + hardLink ? " as hard link" : string.Empty); + + if (!Directory.Exists(Path.GetDirectoryName(destinationFile))) { + Directory.CreateDirectory(Path.GetDirectoryName(destinationFile)); + } + + if (hardLink) { + if (File.Exists(destinationFile)) { + File.Delete(destinationFile); + } + NativeMethods.CreateHardLink(sourceFile, destinationFile); + } else { + File.Copy(sourceFile, destinationFile, true); + } + } + + return !this.Log.HasLoggedErrors; + } + } +} 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/NativeMethods.cs b/src/DotNetOpenAuth.BuildTasks/NativeMethods.cs new file mode 100644 index 0000000..26de3a4 --- /dev/null +++ b/src/DotNetOpenAuth.BuildTasks/NativeMethods.cs @@ -0,0 +1,18 @@ +namespace DotNetOpenAuth.BuildTasks { + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Runtime.InteropServices; + + internal static class NativeMethods { + [DllImport("kernel32", SetLastError = true)] + private static extern bool CreateHardLink(string newFileName, string existingFileName, IntPtr securityAttributes); + + internal static void CreateHardLink(string existingFileName, string newFileName) { + if (!CreateHardLink(newFileName, existingFileName, IntPtr.Zero)) { + Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); + } + } + } +} 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(); + } + } +} |