diff --git a/Packages.props b/Packages.props index cbb091d..f0c566e 100644 --- a/Packages.props +++ b/Packages.props @@ -2,10 +2,13 @@ + + + diff --git a/README.md b/README.md index 1ea654e..5197265 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![NuGet](https://img.shields.io/nuget/v/MSBuild.ProjectCreation.svg)](https://www.nuget.org/packages/MSBuild.ProjectCreation) [![NuGet](https://img.shields.io/nuget/dt/MSBuild.ProjectCreation.svg)](https://www.nuget.org/packages/MSBuild.ProjectCreation) -This class library is a [fluent interface](https://en.wikipedia.org/wiki/Fluent_interface) for generating MSBuild projects. Its primarily for unit tests that need MSBuild projects to do their testing. +This class library is a [fluent interface](https://en.wikipedia.org/wiki/Fluent_interface) for generating MSBuild projects and NuGet package repositories. Its primarily for unit tests that need MSBuild projects to do their testing. ## Example You want to test a custom MSBuild task that you are building so your unit tests need to generate a project that you can build with MSBuild. The following code would generate the necessary project: @@ -125,7 +125,7 @@ Your extension methods should extend the `ProjectCreatorTemplates` class so they ```C# public static class ExtensionMethods { - public ProjectCreator LogsMessage(this ProjectCreatorTemplates template, string text, string path = null, MessageImportance ? importance = null, string condition = null) + public ProjectCreator LogsMessage(this ProjectCreatorTemplates template, string text, string path = null, MessageImportance ? importance = null, string condition = null) { return ProjectCreator.Create(path) .TaskMessage(text, importance, condition); @@ -158,3 +158,32 @@ And the resulting project would look like this: ``` + +# Package Repositories +NuGet and MSBuild are very tightly coupled and a lot of times you need packages available when building projects. + +## Example + +Create a package repository with a package that supports two target frameworks: + +```C# +PackageRepository.Create(rootPath) + .Package("MyPackage", "1.2.3", out PackageIdentify package) + .Library("net472") + .Library("netstandard2.0"); +``` + +The resulting package would have a `lib\net472\MyPackage.dll` and `lib\netstandard2.0\MyPackage.dll` class library. This allows you to restore and build projects that consume the packages + +```C# +PackageRepository.Create(rootPath) + .Package("MyPackage", "1.0.0", out PackageIdentify package) + .Library("netstandard2.0"); + +ProjectCreator.Templates.SdkCsproj() + .ItemPackageReference(package) + .Save(Path.Combine(rootPath, "ClassLibraryA", "ClassLibraryA.csproj")) + .TryBuild(restore: true, out bool result, out BuildOutput buildOutput); +``` + +The result would be a project that references the `MyPackage` package and would restore and build accordingly. \ No newline at end of file diff --git a/src/MSBuildProjectCreator.UnitTests/BuildTests.cs b/src/MSBuildProjectCreator.UnitTests/BuildTests.cs index dc2bb2b..b99cfc4 100644 --- a/src/MSBuildProjectCreator.UnitTests/BuildTests.cs +++ b/src/MSBuildProjectCreator.UnitTests/BuildTests.cs @@ -3,6 +3,7 @@ // Licensed under the MIT license. using Microsoft.Build.Execution; +using NuGet.Packaging.Core; using Shouldly; using System.Collections.Generic; using System.IO; @@ -13,6 +14,29 @@ namespace Microsoft.Build.Utilities.ProjectCreation.UnitTests { public class BuildTests : TestBase { +#if NETCOREAPP + [Fact(Skip = "Does not work yet on .NET Core")] +#else + [Fact] +#endif + public void BuildCanConsumePackage() + { + PackageRepository packageRepository = PackageRepository.Create(TestRootPath) + .Package("PackageB", "1.0", out PackageIdentity packageB) + .Library("net45") + .Package("PackageA", "1.0.0", out PackageIdentity packageA) + .Dependency(packageB, "net45") + .Library("net45"); + + ProjectCreator.Templates.SdkCsproj( + targetFramework: "net45") + .ItemPackageReference(packageA) + .Save(Path.Combine(TestRootPath, "ClassLibraryA", "ClassLibraryA.csproj")) + .TryBuild(restore: true, out bool result, out BuildOutput buildOutput); + + result.ShouldBeTrue(buildOutput.GetConsoleLog()); + } + [Fact] public void BuildTargetOutputsTest() { @@ -33,21 +57,6 @@ public void BuildTargetOutputsTest() item.Value.Items.Select(i => i.ItemSpec).ShouldBe(new[] { "E32099C7AF4E481885B624E5600C718A", "7F38E64414104C6182F492B535926187" }); } - [Fact] - public void RestoreTargetCanBeRun() - { - ProjectCreator - .Create(Path.Combine(TestRootPath, "project1.proj")) - .Target("Restore") - .TaskMessage("312D2E6ABDDC4735B437A016CED1A68E", Framework.MessageImportance.High, condition: "'$(MSBuildRestoreSessionId)' != ''") - .TaskError("MSBuildRestoreSessionId was not defined", condition: "'$(MSBuildRestoreSessionId)' == ''") - .TryRestore(out bool result, out BuildOutput buildOutput); - - result.ShouldBeTrue(buildOutput.GetConsoleLog()); - - buildOutput.MessageEvents.High.ShouldContain(i => i.Message == "312D2E6ABDDC4735B437A016CED1A68E" && i.Importance == Framework.MessageImportance.High, buildOutput.GetConsoleLog()); - } - [Fact] public void CanRestoreAndBuild() { @@ -66,5 +75,20 @@ public void CanRestoreAndBuild() buildOutput.MessageEvents.High.ShouldContain(i => i.Message == "Building...", buildOutput.GetConsoleLog()); } + + [Fact] + public void RestoreTargetCanBeRun() + { + ProjectCreator + .Create(Path.Combine(TestRootPath, "project1.proj")) + .Target("Restore") + .TaskMessage("312D2E6ABDDC4735B437A016CED1A68E", Framework.MessageImportance.High, condition: "'$(MSBuildRestoreSessionId)' != ''") + .TaskError("MSBuildRestoreSessionId was not defined", condition: "'$(MSBuildRestoreSessionId)' == ''") + .TryRestore(out bool result, out BuildOutput buildOutput); + + result.ShouldBeTrue(buildOutput.GetConsoleLog()); + + buildOutput.MessageEvents.High.ShouldContain(i => i.Message == "312D2E6ABDDC4735B437A016CED1A68E" && i.Importance == Framework.MessageImportance.High, buildOutput.GetConsoleLog()); + } } } \ No newline at end of file diff --git a/src/MSBuildProjectCreator.UnitTests/ExtensionMethods.cs b/src/MSBuildProjectCreator.UnitTests/ExtensionMethods.cs new file mode 100644 index 0000000..6da16ba --- /dev/null +++ b/src/MSBuildProjectCreator.UnitTests/ExtensionMethods.cs @@ -0,0 +1,32 @@ +// Copyright (c) Jeff Kluge. All rights reserved. +// +// Licensed under the MIT license. + +using Shouldly; +using System.IO; + +namespace Microsoft.Build.Utilities.ProjectCreation.UnitTests +{ + internal static class ExtensionMethods + { + public static FileInfo ShouldExist(this FileInfo fileInfo) + { + if (!fileInfo.Exists) + { + throw new ShouldAssertException($"The file \"{fileInfo.FullName}\" should exist but does not"); + } + + return fileInfo; + } + + public static DirectoryInfo ShouldExist(this DirectoryInfo directoryInfo) + { + if (!directoryInfo.Exists) + { + throw new ShouldAssertException($"The directory \"{directoryInfo.FullName}\" should exist but does not"); + } + + return directoryInfo; + } + } +} \ No newline at end of file diff --git a/src/MSBuildProjectCreator.UnitTests/PackageRepositoryTests/BuildLogicTests.cs b/src/MSBuildProjectCreator.UnitTests/PackageRepositoryTests/BuildLogicTests.cs new file mode 100644 index 0000000..a9178f7 --- /dev/null +++ b/src/MSBuildProjectCreator.UnitTests/PackageRepositoryTests/BuildLogicTests.cs @@ -0,0 +1,114 @@ +// Copyright (c) Jeff Kluge. All rights reserved. +// +// Licensed under the MIT license. + +using Microsoft.Build.Utilities.ProjectCreation.Resources; +using Shouldly; +using System; +using System.IO; +using Xunit; + +namespace Microsoft.Build.Utilities.ProjectCreation.UnitTests.PackageRepositoryTests +{ + public class BuildLogicTests : TestBase + { + [Fact] + public void BuildLogicRequiresPackage() + { + InvalidOperationException exception; + + exception = Should.Throw(() => + { + PackageRepository.Create(TestRootPath) + .BuildMultiTargetingProps(); + }); + + exception.Message.ShouldBe(Strings.ErrorWhenAddingBuildLogicRequiresPackage); + + exception = Should.Throw(() => + { + PackageRepository.Create(TestRootPath) + .BuildMultiTargetingTargets(); + }); + + exception.Message.ShouldBe(Strings.ErrorWhenAddingBuildLogicRequiresPackage); + + exception = Should.Throw(() => + { + PackageRepository.Create(TestRootPath) + .BuildTransitiveProps(); + }); + + exception.Message.ShouldBe(Strings.ErrorWhenAddingBuildLogicRequiresPackage); + + exception = Should.Throw(() => + { + PackageRepository.Create(TestRootPath) + .BuildTransitiveTargets(); + }); + + exception.Message.ShouldBe(Strings.ErrorWhenAddingBuildLogicRequiresPackage); + + exception = Should.Throw(() => + { + PackageRepository.Create(TestRootPath) + .BuildProps(); + }); + + exception.Message.ShouldBe(Strings.ErrorWhenAddingBuildLogicRequiresPackage); + + exception = Should.Throw(() => + { + PackageRepository.Create(TestRootPath) + .BuildTargets(); + }); + + exception.Message.ShouldBe(Strings.ErrorWhenAddingBuildLogicRequiresPackage); + } + + [Fact] + public void BuildMultiTargetingTest() + { + PackageRepository.Create(TestRootPath) + .Package("PackageA", "2.0") + .BuildMultiTargetingProps(out ProjectCreator buildMultiTargetingPropsProject) + .BuildMultiTargetingTargets(out ProjectCreator buildMultiTargetingTargetsProject); + + buildMultiTargetingPropsProject.FullPath.ShouldBe($@"{TestRootPath}\.nuget\packages\packagea\2.0.0\buildMultiTargeting\PackageA.props"); + buildMultiTargetingTargetsProject.FullPath.ShouldBe($@"{TestRootPath}\.nuget\packages\packagea\2.0.0\buildMultiTargeting\PackageA.targets"); + + File.Exists(buildMultiTargetingPropsProject.FullPath).ShouldBeTrue(); + File.Exists(buildMultiTargetingTargetsProject.FullPath).ShouldBeTrue(); + } + + [Fact] + public void BuildPropsTest() + { + PackageRepository.Create(TestRootPath) + .Package("PackageA", "2.0") + .BuildProps(out ProjectCreator buildPropsProject) + .BuildTargets(out ProjectCreator buildTargetsProject); + + buildPropsProject.FullPath.ShouldBe($@"{TestRootPath}\.nuget\packages\packagea\2.0.0\build\PackageA.props"); + buildTargetsProject.FullPath.ShouldBe($@"{TestRootPath}\.nuget\packages\packagea\2.0.0\build\PackageA.targets"); + + File.Exists(buildPropsProject.FullPath).ShouldBeTrue(); + File.Exists(buildTargetsProject.FullPath).ShouldBeTrue(); + } + + [Fact] + public void BuildTransitiveTest() + { + PackageRepository.Create(TestRootPath) + .Package("PackageA", "2.0") + .BuildTransitiveProps(out ProjectCreator buildTransitivePropsProject) + .BuildTransitiveTargets(out ProjectCreator buildTransitiveTargetsProject); + + buildTransitivePropsProject.FullPath.ShouldBe($@"{TestRootPath}\.nuget\packages\packagea\2.0.0\buildTransitive\PackageA.props"); + buildTransitiveTargetsProject.FullPath.ShouldBe($@"{TestRootPath}\.nuget\packages\packagea\2.0.0\buildTransitive\PackageA.targets"); + + File.Exists(buildTransitivePropsProject.FullPath).ShouldBeTrue(); + File.Exists(buildTransitiveTargetsProject.FullPath).ShouldBeTrue(); + } + } +} \ No newline at end of file diff --git a/src/MSBuildProjectCreator.UnitTests/PackageRepositoryTests/DependencyTests.cs b/src/MSBuildProjectCreator.UnitTests/PackageRepositoryTests/DependencyTests.cs new file mode 100644 index 0000000..c15c185 --- /dev/null +++ b/src/MSBuildProjectCreator.UnitTests/PackageRepositoryTests/DependencyTests.cs @@ -0,0 +1,93 @@ +// Copyright (c) Jeff Kluge. All rights reserved. +// +// Licensed under the MIT license. + +using NuGet.Frameworks; +using NuGet.Packaging; +using NuGet.Packaging.Core; +using NuGet.Versioning; +using Shouldly; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Xunit; + +namespace Microsoft.Build.Utilities.ProjectCreation.UnitTests.PackageRepositoryTests +{ + public class DependencyTests : TestBase + { + [Fact] + public void CanAddDependenciesToMultipleGroups() + { + PackageRepository.Create(TestRootPath) + .Package("PackageA", "1.0.0", out PackageIdentity package) + .Dependency("PackageB", "1.0.0", "net45") + .Dependency("PackageB", "1.0.0", "net46") + .Dependency("PackageB", "1.0.0", "netstandard2.0"); + + ValidatePackageDependencies( + package, + new List + { + new PackageDependencyGroup( + FrameworkConstants.CommonFrameworks.Net45, + new List + { + new PackageDependency("PackageB", VersionRange.Parse("1.0.0")), + }), + new PackageDependencyGroup( + FrameworkConstants.CommonFrameworks.Net46, + new List + { + new PackageDependency("PackageB", VersionRange.Parse("1.0.0")), + }), + new PackageDependencyGroup( + FrameworkConstants.CommonFrameworks.NetStandard20, + new List + { + new PackageDependency("PackageB", VersionRange.Parse("1.0.0")), + }), + }); + } + + [Fact] + public void CanAddMultipleDependenciesToSameGroup() + { + PackageRepository.Create(TestRootPath) + .Package("PackageA", "1.0.0", out PackageIdentity package) + .Dependency("PackageB", "1.0.0", "net45") + .Dependency("PackageC", "1.1.0", "net45") + .Dependency("PackageD", "1.2.0", "net45"); + + ValidatePackageDependencies( + package, + new List + { + new PackageDependencyGroup( + FrameworkConstants.CommonFrameworks.Net45, + new List + { + new PackageDependency("PackageB", VersionRange.Parse("1.0.0")), + new PackageDependency("PackageC", VersionRange.Parse("1.1.0")), + new PackageDependency("PackageD", VersionRange.Parse("1.2.0")), + }), + }); + } + + private void ValidatePackageDependencies(PackageIdentity package, IEnumerable expectedDependencyGroups) + { + FileInfo nuspecFile = new FileInfo(VersionFolderPathResolver.GetManifestFilePath(package.Id, package.Version)); + + nuspecFile.ShouldExist(); + + using (FileStream stream = File.OpenRead(nuspecFile.FullName)) + { + Manifest manifest = Manifest.ReadFrom(stream, validateSchema: false); + + List dependencyGroups = manifest.Metadata.DependencyGroups.ToList(); + + dependencyGroups.ShouldBe(expectedDependencyGroups); + } + } + } +} \ No newline at end of file diff --git a/src/MSBuildProjectCreator.UnitTests/PackageRepositoryTests/LibraryTests.cs b/src/MSBuildProjectCreator.UnitTests/PackageRepositoryTests/LibraryTests.cs new file mode 100644 index 0000000..89e23b2 --- /dev/null +++ b/src/MSBuildProjectCreator.UnitTests/PackageRepositoryTests/LibraryTests.cs @@ -0,0 +1,79 @@ +// Copyright (c) Jeff Kluge. All rights reserved. +// +// Licensed under the MIT license. + +using NuGet.Frameworks; +using NuGet.Packaging.Core; +using Shouldly; +using System; +using System.IO; +using System.Reflection; +using Xunit; + +namespace Microsoft.Build.Utilities.ProjectCreation.UnitTests.PackageRepositoryTests +{ + public class LibraryTests : TestBase + { + [Fact] + public void BasicLibrary() + { + PackageRepository.Create(TestRootPath) + .Package("PackageA", "1.0.0", out PackageIdentity packageA) + .Library(FrameworkConstants.CommonFrameworks.Net45); + + VerifyAssembly(packageA, FrameworkConstants.CommonFrameworks.Net45); + } + + [Fact] + public void LibraryWithVersion() + { + const string assemblyVersion = "2.3.4.5"; + + PackageRepository.Create(TestRootPath) + .Package("PackageA", "1.0.0", out PackageIdentity packageA) + .Library(FrameworkConstants.CommonFrameworks.Net45, assemblyVersion: assemblyVersion); + + VerifyAssembly(packageA, FrameworkConstants.CommonFrameworks.Net45, version: "2.3.4.5"); + } + + [Fact] + public void MultipleLibrariesMultipleTargetFrameworks() + { + PackageRepository.Create(TestRootPath) + .Package("PackageA", "1.0.0", out PackageIdentity packageA) + .Library(FrameworkConstants.CommonFrameworks.Net45) + .Library(FrameworkConstants.CommonFrameworks.NetStandard20); + + VerifyAssembly(packageA, FrameworkConstants.CommonFrameworks.Net45); + VerifyAssembly(packageA, FrameworkConstants.CommonFrameworks.NetStandard20); + } + + [Fact] + public void MultipleLibrariesSameTargetFramework() + { + PackageRepository.Create(TestRootPath) + .Package("PackageA", "1.0.0", out PackageIdentity packageA) + .Library(FrameworkConstants.CommonFrameworks.Net45, filename: null) + .Library(FrameworkConstants.CommonFrameworks.Net45, filename: "CustomAssembly.dll"); + + VerifyAssembly(packageA, FrameworkConstants.CommonFrameworks.Net45); + VerifyAssembly(packageA, FrameworkConstants.CommonFrameworks.Net45, assemblyFileName: "CustomAssembly.dll"); + } + + private void VerifyAssembly(PackageIdentity packageIdentity, NuGetFramework targetFramework, string assemblyFileName = null, string version = null) + { + DirectoryInfo packageDirectory = new DirectoryInfo(VersionFolderPathResolver.GetInstallPath(packageIdentity.Id, packageIdentity.Version)) + .ShouldExist(); + + DirectoryInfo libDirectory = new DirectoryInfo(Path.Combine(packageDirectory.FullName, "lib", targetFramework.GetShortFolderName())) + .ShouldExist(); + + FileInfo classLibrary = new FileInfo(Path.Combine(libDirectory.FullName, assemblyFileName ?? $"{packageIdentity.Id}.dll")) + .ShouldExist(); + + AssemblyName assemblyName = AssemblyName.GetAssemblyName(classLibrary.FullName); + + assemblyName.Version.ShouldBe(version == null ? new Version(1, 0, 0, 0) : Version.Parse(version)); + } + } +} \ No newline at end of file diff --git a/src/MSBuildProjectCreator.UnitTests/PackageRepositoryTests/PackageTests.cs b/src/MSBuildProjectCreator.UnitTests/PackageRepositoryTests/PackageTests.cs new file mode 100644 index 0000000..715f600 --- /dev/null +++ b/src/MSBuildProjectCreator.UnitTests/PackageRepositoryTests/PackageTests.cs @@ -0,0 +1,117 @@ +// Copyright (c) Jeff Kluge. All rights reserved. +// +// Licensed under the MIT license. + +using NuGet.Packaging; +using NuGet.Packaging.Core; +using NuGet.Versioning; +using Shouldly; +using System; +using System.Collections.Generic; +using System.IO; +using Xunit; + +namespace Microsoft.Build.Utilities.ProjectCreation.UnitTests.PackageRepositoryTests +{ + public class PackageTests : TestBase + { + [Fact] + public void BasicPackage() + { + PackageRepository.Create(TestRootPath) + .Package("PackageD", "1.2.3-beta", out PackageIdentity package); + + package.ShouldNotBeNull(); + + package.Id.ShouldBe("PackageD"); + package.Version.ShouldBe(NuGetVersion.Parse("1.2.3-beta")); + + FileInfo manifestFilePath = new FileInfo(VersionFolderPathResolver.GetManifestFilePath(package.Id, package.Version)) + .ShouldExist(); + + using (Stream stream = File.OpenRead(manifestFilePath.FullName)) + { + Manifest manifest = Manifest.ReadFrom(stream, validateSchema: true); + + manifest.Metadata.Authors.ShouldBe(new[] { "UserA" }); + manifest.Metadata.Description.ShouldBe("Description"); + manifest.Metadata.DevelopmentDependency.ShouldBeFalse(); + manifest.Metadata.Id.ShouldBe("PackageD"); + manifest.Metadata.RequireLicenseAcceptance.ShouldBeFalse(); + } + } + + [Fact] + public void CanSetAllPackageProperties() + { + PackageRepository.Create(TestRootPath) + .Package( + name: "PackageD", + version: "1.2.3", + package: out PackageIdentity package, + authors: "UserA;UserB", + description: "Custom description", + copyright: "Copyright 2000", + developmentDependency: true, +#if !NET46 + icon: @"some\icon.jpg", +#endif + iconUrl: "https://icon.url", + language: "Pig latin", + licenseMetadata: new LicenseMetadata(LicenseType.Expression, "MIT", null, null, Version.Parse("1.0.0")), + owners: "Owner1;Owner2", + packageTypes: new List { PackageType.Dependency, PackageType.DotnetCliTool }, + projectUrl: "https://project.url", + releaseNotes: "Release notes for PackageD", + repositoryType: "Git", + repositoryUrl: "https://repository.url", + repositoryBranch: "Branch1000", + repositoryCommit: "Commit14", + requireLicenseAcceptance: true, + serviceable: true, + summary: "Summary of PackageD", + tags: "Tag1 Tag2 Tag3", + title: "Title of PackageD"); + + package.ShouldNotBeNull(); + + package.Id.ShouldBe("PackageD"); + package.Version.ShouldBe(NuGetVersion.Parse("1.2.3")); + + FileInfo manifestFilePath = new FileInfo(VersionFolderPathResolver.GetManifestFilePath(package.Id, package.Version)) + .ShouldExist(); + + using (Stream stream = File.OpenRead(manifestFilePath.FullName)) + { + Manifest manifest = Manifest.ReadFrom(stream, validateSchema: true); + + manifest.Metadata.Authors.ShouldBe(new[] { "UserA", "UserB" }); + manifest.Metadata.Copyright.ShouldBe("Copyright 2000"); + manifest.Metadata.Description.ShouldBe("Custom description"); + manifest.Metadata.DevelopmentDependency.ShouldBeTrue(); +#if !NET46 + manifest.Metadata.Icon.ShouldBe(@"some\icon.jpg"); +#endif + manifest.Metadata.IconUrl.ShouldBe(new Uri("https://icon.url")); + manifest.Metadata.Id.ShouldBe("PackageD"); + manifest.Metadata.Language.ShouldBe("Pig latin"); + manifest.Metadata.LicenseMetadata.License.ShouldBe("MIT"); + manifest.Metadata.LicenseMetadata.Type.ShouldBe(LicenseType.Expression); + manifest.Metadata.LicenseMetadata.Version.ShouldBe(Version.Parse("1.0.0")); + manifest.Metadata.Owners.ShouldBe(new[] { "Owner1", "Owner2" }); + manifest.Metadata.PackageTypes.ShouldBe(new[] { PackageType.Dependency, PackageType.DotnetCliTool }); + manifest.Metadata.ProjectUrl.ShouldBe(new Uri("https://project.url")); + manifest.Metadata.ReleaseNotes.ShouldBe("Release notes for PackageD"); + manifest.Metadata.Repository.Branch.ShouldBe("Branch1000"); + manifest.Metadata.Repository.Commit.ShouldBe("Commit14"); + manifest.Metadata.Repository.Type.ShouldBe("Git"); + manifest.Metadata.Repository.Url.ShouldBe("https://repository.url"); + manifest.Metadata.RequireLicenseAcceptance.ShouldBeTrue(); + manifest.Metadata.Serviceable.ShouldBeTrue(); + manifest.Metadata.Summary.ShouldBe("Summary of PackageD"); + manifest.Metadata.Tags.ShouldBe("Tag1 Tag2 Tag3"); + manifest.Metadata.Title.ShouldBe("Title of PackageD"); + } + } + } +} \ No newline at end of file diff --git a/src/MSBuildProjectCreator.UnitTests/TestBase.cs b/src/MSBuildProjectCreator.UnitTests/TestBase.cs index 102398e..14c749a 100644 --- a/src/MSBuildProjectCreator.UnitTests/TestBase.cs +++ b/src/MSBuildProjectCreator.UnitTests/TestBase.cs @@ -2,15 +2,23 @@ // // Licensed under the MIT license. +using NuGet.Packaging; using System; using System.IO; namespace Microsoft.Build.Utilities.ProjectCreation.UnitTests { - public abstract class TestBase : MSBuildTestBase + public abstract class TestBase : MSBuildTestBase, IDisposable { + private readonly Lazy _pathResolverLazy; + private readonly string _testRootPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + protected TestBase() + { + _pathResolverLazy = new Lazy(() => new VersionFolderPathResolver(Path.Combine(TestRootPath, ".nuget", "packages"))); + } + public string TestRootPath { get @@ -20,6 +28,8 @@ public string TestRootPath } } + public VersionFolderPathResolver VersionFolderPathResolver => _pathResolverLazy.Value; + public void Dispose() { Dispose(true); diff --git a/src/MSBuildProjectCreator/MSBuildProjectCreator.csproj b/src/MSBuildProjectCreator/MSBuildProjectCreator.csproj index f502351..5bfbe4b 100644 --- a/src/MSBuildProjectCreator/MSBuildProjectCreator.csproj +++ b/src/MSBuildProjectCreator/MSBuildProjectCreator.csproj @@ -35,6 +35,18 @@ + + + + + + + + + + + + ResXFileCodeGenerator diff --git a/src/MSBuildProjectCreator/PackageManifest.cs b/src/MSBuildProjectCreator/PackageManifest.cs new file mode 100644 index 0000000..ca3048f --- /dev/null +++ b/src/MSBuildProjectCreator/PackageManifest.cs @@ -0,0 +1,339 @@ +// Copyright (c) Jeff Kluge. All rights reserved. +// +// Licensed under the MIT license. + +using NuGet.Common; +using NuGet.Frameworks; +using NuGet.Packaging; +using NuGet.Packaging.Core; +using NuGet.Versioning; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Microsoft.Build.Utilities.ProjectCreation +{ + /// + /// Represents the manifest of a package. + /// + internal class PackageManifest : Manifest + { +#if NET46 + /// + /// Initializes a new instance of the class. + /// + /// The full path to the manifest file. + /// The name or ID of the package. + /// The semantic version of the package. + /// An optional semicolon delimited list of authors of the package. The default value is "UserA" + /// An optional description of the package. The default value is "Description" + /// An optional copyright of the package. + /// An optional value indicating whether or not the package is a development dependency. The default value is false. + /// An optional URL to the icon of the package. + /// An optional language of the package. + /// An optional URL to the license of the package. + /// An optional of the package. + /// An optional semicolon delimited list of owners of the package. + /// An optional containing the package types of the package. + /// An optional URL to the project of the package. + /// An optional value specifying release notes of the package. + /// An optional value specifying the type of source code repository of the package. + /// An optional value specifying the URL of the source code repository of the package. + /// An optional value specifying the branch of the source code repository of the package. + /// An optional value specifying the commit of the source code repository of the package. + /// An optional value indicating whether or not the package requires license acceptance The default value is false. + /// An option value indicating whether or not the package is serviceable. The default value is false. + /// An optional summary of the package. + /// An optional set of tags of the package. + /// An optional title of the package. +#else + /// + /// Initializes a new instance of the class. + /// + /// The full path to the manifest file. + /// The name or ID of the package. + /// The semantic version of the package. + /// An optional semicolon delimited list of authors of the package. The default value is "UserA" + /// An optional description of the package. The default value is "Description" + /// An optional copyright of the package. + /// An optional value indicating whether or not the package is a development dependency. The default value is false. + /// An optional path in the package that should be used for the icon of the package. + /// An optional URL to the icon of the package. + /// An optional language of the package. + /// An optional URL to the license of the package. + /// An optional of the package. + /// An optional semicolon delimited list of owners of the package. + /// An optional containing the package types of the package. + /// An optional URL to the project of the package. + /// An optional value specifying release notes of the package. + /// An optional value specifying the type of source code repository of the package. + /// An optional value specifying the URL of the source code repository of the package. + /// An optional value specifying the branch of the source code repository of the package. + /// An optional value specifying the commit of the source code repository of the package. + /// An optional value indicating whether or not the package requires license acceptance The default value is false. + /// An option value indicating whether or not the package is serviceable. The default value is false. + /// An optional summary of the package. + /// An optional set of tags of the package. + /// An optional title of the package. +#endif + + public PackageManifest( + string fullPath, + string name, + string version, + string authors = null, + string description = null, + string copyright = null, + bool developmentDependency = false, +#if !NET46 + string icon = null, +#endif + string iconUrl = null, + string language = null, + string licenseUrl = null, + LicenseMetadata licenseMetadata = null, + string owners = null, + IEnumerable packageTypes = null, + string projectUrl = null, + string releaseNotes = null, + string repositoryType = null, + string repositoryUrl = null, + string repositoryBranch = null, + string repositoryCommit = null, + bool requireLicenseAcceptance = false, + bool serviceable = false, + string summary = null, + string tags = null, + string title = null) + : base( + GetManifestMetadata( + name, + version, + authors, + description, + copyright, + developmentDependency, +#if !NET46 + icon, +#endif + iconUrl, + language, + licenseUrl, + licenseMetadata, + owners, + packageTypes, + projectUrl, + releaseNotes, + repositoryType, + repositoryUrl, + repositoryBranch, + repositoryCommit, + requireLicenseAcceptance, + serviceable, + summary, + tags, + title)) + { + if (string.IsNullOrWhiteSpace(fullPath)) + { + throw new ArgumentNullException(nameof(fullPath)); + } + + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + if (string.IsNullOrWhiteSpace(version)) + { + throw new ArgumentNullException(nameof(version)); + } + + FullPath = fullPath; + + Directory = Path.GetDirectoryName(fullPath); + + Save(); + + NupkgMetadataFileFormat.Write( + Path.Combine(Directory, PackagingCoreConstants.NupkgMetadataFileExtension), + new NupkgMetadataFile + { + ContentHash = string.Empty, + Version = NupkgMetadataFileFormat.Version, + }); + } + + /// + /// Gets the full path to the directory of the package. + /// + public string Directory { get; } + + /// + /// Gets the full path to the package manifest file. + /// + public string FullPath { get; } + + /// + /// Adds a to the package manifest. + /// + /// The of the package dependency group. + public void AddDependencyGroup(NuGetFramework targetFramework) + { + Metadata.DependencyGroups = Metadata.DependencyGroups.Concat(new List + { + new PackageDependencyGroup(targetFramework, Enumerable.Empty()), + }); + + Save(); + } + + /// + /// Saves the package manifest file. + /// + public void Save() + { + System.IO.Directory.CreateDirectory(Directory); + + using (Stream stream = new FileStream(FullPath, FileMode.Create)) + { + Save(stream); + } + } + +#if NET46 + /// + /// Gets the for a package. + /// + /// + /// Initializes a new instance of the class. + /// + /// The name or ID of the package. + /// The semantic version of the package. + /// An optional semicolon delimited list of authors of the package. The default value is "UserA" + /// An optional description of the package. The default value is "Description" + /// An optional copyright of the package. + /// An optional value indicating whether or not the package is a development dependency. The default value is false. + /// An optional URL to the icon of the package. + /// An optional language of the package. + /// An optional URL to the license of the package. + /// An optional of the package. + /// An optional semicolon delimited list of owners of the package. + /// An optional containing the package types of the package. + /// An optional URL to the project of the package. + /// An optional value specifying release notes of the package. + /// An optional value specifying the type of source code repository of the package. + /// An optional value specifying the URL of the source code repository of the package. + /// An optional value specifying the branch of the source code repository of the package. + /// An optional value specifying the commit of the source code repository of the package. + /// An optional value indicating whether or not the package requires license acceptance The default value is false. + /// An option value indicating whether or not the package is serviceable. The default value is false. + /// An optional summary of the package. + /// An optional set of tags of the package. + /// An optional title of the package. + /// The for the package. +#else + /// + /// Gets the for a package. + /// + /// + /// Initializes a new instance of the class. + /// + /// The name or ID of the package. + /// The semantic version of the package. + /// An optional semicolon delimited list of authors of the package. The default value is "UserA" + /// An optional description of the package. The default value is "Description" + /// An optional copyright of the package. + /// An optional value indicating whether or not the package is a development dependency. The default value is false. + /// An optional path in the package that should be used for the icon of the package. + /// An optional URL to the icon of the package. + /// An optional language of the package. + /// An optional URL to the license of the package. + /// An optional of the package. + /// An optional semicolon delimited list of owners of the package. + /// An optional containing the package types of the package. + /// An optional URL to the project of the package. + /// An optional value specifying release notes of the package. + /// An optional value specifying the type of source code repository of the package. + /// An optional value specifying the URL of the source code repository of the package. + /// An optional value specifying the branch of the source code repository of the package. + /// An optional value specifying the commit of the source code repository of the package. + /// An optional value indicating whether or not the package requires license acceptance The default value is false. + /// An option value indicating whether or not the package is serviceable. The default value is false. + /// An optional summary of the package. + /// An optional set of tags of the package. + /// An optional title of the package. + /// The for the package. +#endif + private static ManifestMetadata GetManifestMetadata( + string name, + string version, + string authors = null, + string description = null, + string copyright = null, + bool developmentDependency = false, +#if !NET46 + string icon = null, +#endif + string iconUrl = null, + string language = null, + string licenseUrl = null, + LicenseMetadata licenseMetadata = null, + string owners = null, + IEnumerable packageTypes = null, + string projectUrl = null, + string releaseNotes = null, + string repositoryType = null, + string repositoryUrl = null, + string repositoryBranch = null, + string repositoryCommit = null, + bool requireLicenseAcceptance = false, + bool serviceable = false, + string summary = null, + string tags = null, + string title = null) + { + ManifestMetadata metadata = new ManifestMetadata + { + Authors = MSBuildStringUtility.Split(authors ?? "UserA"), + Copyright = copyright, + Description = description ?? "Description", + DevelopmentDependency = developmentDependency, +#if !NET46 + Icon = icon, +#endif + Id = name, + Language = language, + LicenseMetadata = licenseMetadata, + Owners = string.IsNullOrWhiteSpace(owners) ? null : MSBuildStringUtility.Split(owners), + PackageTypes = packageTypes ?? new[] { PackageType.Dependency }, + ReleaseNotes = releaseNotes, + Repository = new RepositoryMetadata(repositoryType, repositoryUrl, repositoryBranch, repositoryCommit), + RequireLicenseAcceptance = requireLicenseAcceptance, + Serviceable = serviceable, + Summary = summary, + Tags = tags, + Title = title, + Version = NuGetVersion.Parse(version), + }; + + if (!string.IsNullOrWhiteSpace(iconUrl)) + { + metadata.SetIconUrl(iconUrl); + } + + if (!string.IsNullOrWhiteSpace(licenseUrl)) + { + metadata.SetLicenseUrl(licenseUrl); + } + + if (!string.IsNullOrWhiteSpace(projectUrl)) + { + metadata.SetProjectUrl(projectUrl); + } + + return metadata; + } + } +} \ No newline at end of file diff --git a/src/MSBuildProjectCreator/PackageRepository.BuildLogic.cs b/src/MSBuildProjectCreator/PackageRepository.BuildLogic.cs new file mode 100644 index 0000000..82244c2 --- /dev/null +++ b/src/MSBuildProjectCreator/PackageRepository.BuildLogic.cs @@ -0,0 +1,316 @@ +// Copyright (c) Jeff Kluge. All rights reserved. +// +// Licensed under the MIT license. + +using Microsoft.Build.Evaluation; +using Microsoft.Build.Utilities.ProjectCreation.Resources; +using System; +using System.IO; + +namespace Microsoft.Build.Utilities.ProjectCreation +{ + public partial class PackageRepository + { + /// + /// Adds a .props file to the buildMultitargeting directory. + /// + /// Optional for the .props file. + /// The current . + public PackageRepository BuildMultiTargetingProps(NewProjectFileOptions projectFileOptions = NewProjectFileOptions.IncludeAllOptions) + { + return BuildMultiTargetingProps(null, projectFileOptions); + } + + /// + /// Adds a .props file to the buildMultitargeting directory. + /// + /// An to generate the .props file. + /// Optional for the .props file. + /// The current . + public PackageRepository BuildMultiTargetingProps(Action creator, NewProjectFileOptions projectFileOptions = NewProjectFileOptions.IncludeAllOptions) + { + return BuildMultiTargetingProps(creator, out _, projectFileOptions); + } + + /// + /// Adds a .props file to the buildMultitargeting directory. + /// + /// Receives the of the created project file. + /// Optional for the .props file. + /// The current . + public PackageRepository BuildMultiTargetingProps(out ProjectCreator project, NewProjectFileOptions projectFileOptions = NewProjectFileOptions.IncludeAllOptions) + { + return BuildMultiTargetingProps(null, out project, projectFileOptions); + } + + /// + /// Adds a .props file to the buildMultitargeting directory. + /// + /// An to generate the .props file. + /// Receives the of the created project file. + /// Optional for the .props file. + /// The current . + public PackageRepository BuildMultiTargetingProps(Action creator, out ProjectCreator project, NewProjectFileOptions projectFileOptions = NewProjectFileOptions.IncludeAllOptions) + { + CreateBuildFile(".props", "buildMultiTargeting", creator, projectFileOptions, out project); + + return this; + } + + /// + /// Adds a .targets file to the buildMultitargeting directory. + /// + /// Optional for the .targets file. + /// The current . + public PackageRepository BuildMultiTargetingTargets(NewProjectFileOptions projectFileOptions = NewProjectFileOptions.IncludeAllOptions) + { + return BuildMultiTargetingTargets(out _, projectFileOptions); + } + + /// + /// Adds a .targets file to the buildMultitargeting directory. + /// + /// Receives the of the created project file. + /// Optional for the .targets file. + /// The current . + public PackageRepository BuildMultiTargetingTargets(out ProjectCreator project, NewProjectFileOptions projectFileOptions = NewProjectFileOptions.IncludeAllOptions) + { + return BuildMultiTargetingTargets(null, out project, projectFileOptions); + } + + /// + /// Adds a .targets file to the buildMultitargeting directory. + /// + /// An to generate the .targets file. + /// Optional for the .targets file. + /// The current . + public PackageRepository BuildMultiTargetingTargets(Action creator, NewProjectFileOptions projectFileOptions = NewProjectFileOptions.IncludeAllOptions) + { + return BuildMultiTargetingTargets(creator, out _, projectFileOptions); + } + + /// + /// Adds a .targets file to the buildMultitargeting directory. + /// + /// An to generate the .targets file. + /// Receives the of the created project file. + /// Optional for the .targets file. + /// The current . + public PackageRepository BuildMultiTargetingTargets(Action creator, out ProjectCreator project, NewProjectFileOptions projectFileOptions = NewProjectFileOptions.IncludeAllOptions) + { + CreateBuildFile(".targets", "buildMultiTargeting", creator, projectFileOptions, out project); + + return this; + } + + /// + /// Adds a .props file to the build directory. + /// + /// Optional for the .props file. + /// The current . + public PackageRepository BuildProps(NewProjectFileOptions projectFileOptions = NewProjectFileOptions.IncludeAllOptions) + { + return BuildProps(out ProjectCreator _, projectFileOptions); + } + + /// + /// Adds a .props file to the build directory. + /// + /// Receives the of the created project file. + /// Optional for the .props file. + /// The current . + public PackageRepository BuildProps(out ProjectCreator project, NewProjectFileOptions projectFileOptions = NewProjectFileOptions.IncludeAllOptions) + { + return BuildProps(null, out project, projectFileOptions); + } + + /// + /// Adds a .props file to the build directory. + /// + /// An to generate the .props file. + /// Optional for the .props file. + /// The current . + public PackageRepository BuildProps(Action creator, NewProjectFileOptions projectFileOptions = NewProjectFileOptions.IncludeAllOptions) + { + return BuildProps(creator, out ProjectCreator _, projectFileOptions); + } + + /// + /// Adds a .props file to the build directory. + /// + /// An to generate the .props file. + /// Receives the of the created project file. + /// Optional for the .props file. + /// The current . + public PackageRepository BuildProps(Action creator, out ProjectCreator project, NewProjectFileOptions projectFileOptions = NewProjectFileOptions.IncludeAllOptions) + { + CreateBuildFile(".props", "build", creator, projectFileOptions, out project); + + return this; + } + + /// + /// Adds a .targets file to the build directory. + /// + /// Optional for the .targets file. + /// The current . + public PackageRepository BuildTargets(NewProjectFileOptions projectFileOptions = NewProjectFileOptions.IncludeAllOptions) + { + return BuildTargets(out _, projectFileOptions); + } + + /// + /// Adds a .targets file to the build directory. + /// + /// Receives the of the created project file. + /// Optional for the .targets file. + /// The current . + public PackageRepository BuildTargets(out ProjectCreator project, NewProjectFileOptions projectFileOptions = NewProjectFileOptions.IncludeAllOptions) + { + return BuildTargets(null, out project, projectFileOptions); + } + + /// + /// Adds a .targets file to the build directory. + /// + /// An to generate the .targets file. + /// Optional for the .targets file. + /// The current . + public PackageRepository BuildTargets(Action creator, NewProjectFileOptions projectFileOptions = NewProjectFileOptions.IncludeAllOptions) + { + return BuildTargets(creator, out _, projectFileOptions); + } + + /// + /// Adds a .targets file to the build directory. + /// + /// An to generate the .targets file. + /// Receives the of the created project file. + /// Optional for the .targets file. + /// The current . + public PackageRepository BuildTargets(Action creator, out ProjectCreator project, NewProjectFileOptions projectFileOptions = NewProjectFileOptions.IncludeAllOptions) + { + CreateBuildFile(".targets", "build", creator, projectFileOptions, out project); + + return this; + } + + /// + /// Adds a .props file to the buildTransitive directory. + /// + /// Optional for the .props file. + /// The current . + public PackageRepository BuildTransitiveProps(NewProjectFileOptions projectFileOptions = NewProjectFileOptions.IncludeAllOptions) + { + return BuildTransitiveProps(out _, projectFileOptions); + } + + /// + /// Adds a .props file to the buildTransitive directory. + /// + /// Receives the of the created project file. + /// Optional for the .props file. + /// The current . + public PackageRepository BuildTransitiveProps(out ProjectCreator project, NewProjectFileOptions projectFileOptions = NewProjectFileOptions.IncludeAllOptions) + { + return BuildTransitiveProps(null, out project, projectFileOptions); + } + + /// + /// Adds a .props file to the buildTransitive directory. + /// + /// An to generate the .props file. + /// Optional for the .props file. + /// The current . + public PackageRepository BuildTransitiveProps(Action creator, NewProjectFileOptions projectFileOptions = NewProjectFileOptions.IncludeAllOptions) + { + return BuildTransitiveProps(creator, out _, projectFileOptions); + } + + /// + /// Adds a .props file to the buildTransitive directory. + /// + /// An to generate the .props file. + /// Receives the of the created project file. + /// Optional for the .props file. + /// The current . + public PackageRepository BuildTransitiveProps(Action creator, out ProjectCreator project, NewProjectFileOptions projectFileOptions = NewProjectFileOptions.IncludeAllOptions) + { + CreateBuildFile(".props", "buildTransitive", creator, projectFileOptions, out project); + + return this; + } + + /// + /// Adds a .targets file to the buildTransitive directory. + /// + /// Optional for the .targets file. + /// The current . + public PackageRepository BuildTransitiveTargets(NewProjectFileOptions projectFileOptions = NewProjectFileOptions.IncludeAllOptions) + { + return BuildTransitiveTargets(out _, projectFileOptions); + } + + /// + /// Adds a .targets file to the buildTransitive directory. + /// + /// Receives the of the created project file. + /// Optional for the .targets file. + /// The current . + public PackageRepository BuildTransitiveTargets(out ProjectCreator project, NewProjectFileOptions projectFileOptions = NewProjectFileOptions.IncludeAllOptions) + { + return BuildTransitiveTargets(null, out project, projectFileOptions); + } + + /// + /// Adds a .targets file to the buildTransitive directory. + /// + /// An to generate the .targets file. + /// Optional for the .targets file. + /// The current . + public PackageRepository BuildTransitiveTargets(Action creator, NewProjectFileOptions projectFileOptions = NewProjectFileOptions.IncludeAllOptions) + { + return BuildTransitiveTargets(creator, out _, projectFileOptions); + } + + /// + /// Adds a .targets file to the buildTransitive directory. + /// + /// An to generate the .targets file. + /// Receives the of the created project file. + /// Optional for the .targets file. + /// The current . + public PackageRepository BuildTransitiveTargets(Action creator, out ProjectCreator project, NewProjectFileOptions projectFileOptions = NewProjectFileOptions.IncludeAllOptions) + { + CreateBuildFile(".targets", "buildTransitive", creator, projectFileOptions, out project); + + return this; + } + + private void CreateBuildFile(string extension, string folderName, Action creator, NewProjectFileOptions projectFileOptions, out ProjectCreator project) + { + if (_packageManifest == null) + { + throw new InvalidOperationException(Strings.ErrorWhenAddingBuildLogicRequiresPackage); + } + + if (string.IsNullOrWhiteSpace(extension)) + { + throw new ArgumentNullException(extension); + } + + if (string.IsNullOrWhiteSpace(folderName)) + { + throw new ArgumentNullException(folderName); + } + + project = ProjectCreator.Create( + path: Path.Combine(_packageManifest.Directory, folderName, $"{_packageManifest.Metadata.Id}{extension}"), + projectFileOptions: projectFileOptions); + + creator?.Invoke(project); + + project.Save(); + } + } +} \ No newline at end of file diff --git a/src/MSBuildProjectCreator/PackageRepository.Dependency.cs b/src/MSBuildProjectCreator/PackageRepository.Dependency.cs new file mode 100644 index 0000000..bac60f6 --- /dev/null +++ b/src/MSBuildProjectCreator/PackageRepository.Dependency.cs @@ -0,0 +1,117 @@ +// Copyright (c) Jeff Kluge. All rights reserved. +// +// Licensed under the MIT license. + +using NuGet.Common; +using NuGet.Frameworks; +using NuGet.LibraryModel; +using NuGet.Packaging; +using NuGet.Packaging.Core; +using NuGet.Versioning; +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Build.Utilities.ProjectCreation +{ + public partial class PackageRepository + { + /// + /// Adds a dependency to the current package. + /// + /// The of the dependency. + /// An optional target framework for the dependency. + /// An optional representing the assets to include for the dependency. + /// An optional representing the assets to exclude from the dependency. + /// The current . + public PackageRepository Dependency(PackageIdentity packageIdentity, string targetFramework, LibraryIncludeFlags? include = null, LibraryIncludeFlags? exclude = null) + { + return Dependency( + packageIdentity.Id, + new VersionRange(packageIdentity.Version), + targetFramework, + include, + exclude); + } + + /// + /// Adds a dependency to the current package. + /// + /// The of the dependency. + /// The for the dependency. + /// An optional representing the assets to include for the dependency. + /// An optional representing the assets to exclude from the dependency. + /// The current . + public PackageRepository Dependency(PackageIdentity packageIdentity, NuGetFramework targetFramework, LibraryIncludeFlags? include = null, LibraryIncludeFlags? exclude = null) + { + return Dependency(packageIdentity.Id, new VersionRange(packageIdentity.Version), targetFramework, include, exclude); + } + + /// + /// Adds a dependency to the current package. + /// + /// The identifier of the dependency. + /// The version of the dependency. + /// An optional target framework for the dependency. + /// An optional representing the assets to include for the dependency. + /// An optional representing the assets to exclude from the dependency. + /// The current . + public PackageRepository Dependency(string id, string version, string targetFramework, LibraryIncludeFlags? include = null, LibraryIncludeFlags? exclude = null) + { + return Dependency( + id, + VersionRange.Parse(version), + targetFramework, + include, + exclude); + } + + /// + /// Adds a dependency to the current package. + /// + /// The identifier of the dependency. + /// The of the dependency. + /// An optional target framework for the dependency. + /// An optional representing the assets to include for the dependency. + /// An optional representing the assets to exclude from the dependency. + /// The current . + public PackageRepository Dependency(string id, VersionRange version, string targetFramework, LibraryIncludeFlags? include = null, LibraryIncludeFlags? exclude = null) + { + return Dependency( + id, + version, + string.IsNullOrWhiteSpace(targetFramework) ? null : NuGetFramework.Parse(targetFramework), + include, + exclude); + } + + /// + /// Adds a dependency to the current package. + /// + /// The identifier of the dependency. + /// The of the dependency. + /// The for the dependency. + /// An optional representing the assets to include for the dependency. + /// An optional representing the assets to exclude from the dependency. + /// The current . + public PackageRepository Dependency(string id, VersionRange version, NuGetFramework targetFramework, LibraryIncludeFlags? include = null, LibraryIncludeFlags? exclude = null) + { + _packageManifest.Metadata.DependencyGroups = _packageManifest.Metadata.DependencyGroups.Concat(new List + { + new PackageDependencyGroup( + targetFramework ?? NuGetFramework.AnyFramework, + new List + { + new PackageDependency( + id, + version, + include == null ? null : MSBuildStringUtility.Split(include.ToString(), ','), + exclude == null ? null : MSBuildStringUtility.Split(exclude.ToString(), ',')), + }), + }); + + _packageManifest.Save(); + + return this; + } + } +} \ No newline at end of file diff --git a/src/MSBuildProjectCreator/PackageRepository.File.cs b/src/MSBuildProjectCreator/PackageRepository.File.cs new file mode 100644 index 0000000..b4dd8d7 --- /dev/null +++ b/src/MSBuildProjectCreator/PackageRepository.File.cs @@ -0,0 +1,46 @@ +// Copyright (c) Jeff Kluge. All rights reserved. +// +// Licensed under the MIT license. + +using Microsoft.Build.Utilities.ProjectCreation.Resources; +using System; +using System.Globalization; +using System.IO; + +namespace Microsoft.Build.Utilities.ProjectCreation +{ + public partial class PackageRepository + { + /// + /// Adds a text file to the package. + /// + /// The relative path of the text file within the package. + /// The contents of the text file. + /// The current . + public PackageRepository FileText(string relativePath, string contents) + { + return File(relativePath, file => System.IO.File.WriteAllText(file.FullName, contents)); + } + + private PackageRepository File(string relativePath, Action writeAction) + { + FileInfo fileInfo = new FileInfo(Path.Combine(_packageManifest.Directory, relativePath)); + + if (fileInfo.Exists) + { + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Strings.ErrorFileAlreadyCreated, relativePath)); + } + + if (fileInfo.Directory == null) + { + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Strings.ErrorFilePathMustBeInADirectory, relativePath)); + } + + fileInfo.Directory.Create(); + + writeAction(fileInfo); + + return this; + } + } +} \ No newline at end of file diff --git a/src/MSBuildProjectCreator/PackageRepository.Library.cs b/src/MSBuildProjectCreator/PackageRepository.Library.cs new file mode 100644 index 0000000..1aea158 --- /dev/null +++ b/src/MSBuildProjectCreator/PackageRepository.Library.cs @@ -0,0 +1,118 @@ +// Copyright (c) Jeff Kluge. All rights reserved. +// +// Licensed under the MIT license. + +using Microsoft.Build.Utilities.ProjectCreation.Resources; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Emit; +using NuGet.Frameworks; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Microsoft.Build.Utilities.ProjectCreation +{ + public partial class PackageRepository + { + /// + /// Adds a library to the package. + /// + /// The target framework of the library. + /// An optional filename for the library. The default value is <PackageId>.dll. + /// An optional namespace for the library. The default value is <PackageId>. + /// An optional class name for the library. The default value is <PackageId>_Class. + /// An optional assembly version for the library. The default value is "1.0.0.0" + /// The current . + public PackageRepository Library(string targetFramework, string filename = null, string @namespace = null, string className = null, string assemblyVersion = "1.0.0.0") + { + return Library(NuGetFramework.Parse(targetFramework), filename, @namespace, className, assemblyVersion); + } + + /// + /// Adds a library to the package. + /// + /// The of the library. + /// An optional filename for the library. The default value is <PackageId>.dll. + /// An optional namespace for the library. The default value is <PackageId>. + /// An optional class name for the library. The default value is <PackageId>_Class. + /// An optional assembly version for the library. The default value is "1.0.0.0" + /// The current . + public PackageRepository Library(NuGetFramework targetFramework, string filename = null, string @namespace = null, string className = null, string assemblyVersion = "1.0.0.0") + { + if (_packageManifest == null) + { + throw new InvalidOperationException(Strings.ErrorWhenAddingLibraryRequiresPackage); + } + + if (string.IsNullOrWhiteSpace(filename)) + { + filename = $"{_packageManifest.Metadata.Id}.dll"; + } + + if (string.IsNullOrWhiteSpace(className)) + { + className = $"{_packageManifest.Metadata.Id}_Class"; + } + + _packageManifest.AddDependencyGroup(targetFramework); + + return File( + Path.Combine("lib", targetFramework.GetShortFolderName(), filename), + fileInfo => CreateAssembly(fileInfo, @namespace, className, assemblyVersion, targetFramework.GetDotNetFrameworkName(DefaultFrameworkNameProvider.Instance))); + } + + private void CreateAssembly(FileInfo fileInfo, string @namespace, string className, string version, string targetFramework) + { + if (fileInfo == null) + { + throw new ArgumentNullException(nameof(fileInfo)); + } + + if (fileInfo.Directory == null) + { + throw new ArgumentNullException(nameof(fileInfo.Directory)); + } + + fileInfo.Directory.Create(); + + string name = string.IsNullOrWhiteSpace(@namespace) ? Path.GetFileNameWithoutExtension(fileInfo.Name) : @namespace; + + CreateAssembly( + fileInfo, + name, + $@" +[assembly: System.Reflection.AssemblyVersionAttribute(""{version}"")] +[assembly: System.Runtime.Versioning.TargetFramework(""{targetFramework}"")] +namespace {name} +{{ + public class {className} + {{ + }} +}}", + new[] + { + typeof(object).Assembly.Location, + }); + } + + private void CreateAssembly(FileInfo fileInfo, string name, string code, IEnumerable references, OutputKind outputKind = OutputKind.DynamicallyLinkedLibrary) + { + CSharpCompilation compilation = CSharpCompilation.Create( + name, + new[] + { + CSharpSyntaxTree.ParseText(code), + }, + references.Select(i => MetadataReference.CreateFromFile(i)), + new CSharpCompilationOptions(outputKind)); + + EmitResult result = compilation.Emit(fileInfo.FullName); + + if (!result.Success) + { + } + } + } +} \ No newline at end of file diff --git a/src/MSBuildProjectCreator/PackageRepository.Package.cs b/src/MSBuildProjectCreator/PackageRepository.Package.cs new file mode 100644 index 0000000..e170e2d --- /dev/null +++ b/src/MSBuildProjectCreator/PackageRepository.Package.cs @@ -0,0 +1,289 @@ +// Copyright (c) Jeff Kluge. All rights reserved. +// +// Licensed under the MIT license. + +using Microsoft.Build.Utilities.ProjectCreation.Resources; +using NuGet.Packaging; +using NuGet.Packaging.Core; +using NuGet.Versioning; +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace Microsoft.Build.Utilities.ProjectCreation +{ + public partial class PackageRepository + { + private readonly HashSet _packages = new HashSet(); + + /// + /// Gets the current packages in the repository. + /// + public IReadOnlyCollection Packages => _packages; + + /// + public string GetInstallPath(string packageId, NuGetVersion version) + { + return _versionFolderPathResolver.GetInstallPath(packageId, version); + } + +#if NET46 + /// + /// Creates a new package. + /// + /// The name or ID of the package. + /// The semantic version of the package. + /// An optional semicolon delimited list of authors of the package. The default value is "UserA" + /// An optional description of the package. The default value is "Description" + /// An optional copyright of the package. + /// An optional value indicating whether or not the package is a development dependency. The default value is false. + /// An optional URL to the icon of the package. + /// An optional language of the package. + /// An optional URL to the license of the package. + /// An optional of the package. + /// An optional semicolon delimited list of owners of the package. + /// An optional containing the package types of the package. + /// An optional URL to the project of the package. + /// An optional value specifying release notes of the package. + /// An optional value specifying the type of source code repository of the package. + /// An optional value specifying the URL of the source code repository of the package. + /// An optional value specifying the branch of the source code repository of the package. + /// An optional value specifying the commit of the source code repository of the package. + /// An optional value indicating whether or not the package requires license acceptance The default value is false. + /// An option value indicating whether or not the package is serviceable. The default value is false. + /// An optional summary of the package. + /// An optional set of tags of the package. + /// An optional title of the package. + /// The current . +#else + /// + /// Creates a new package. + /// + /// The name or ID of the package. + /// The semantic version of the package. + /// An optional semicolon delimited list of authors of the package. The default value is "UserA" + /// An optional description of the package. The default value is "Description" + /// An optional copyright of the package. + /// An optional value indicating whether or not the package is a development dependency. The default value is false. + /// An optional path in the package that should be used for the icon of the package. + /// An optional URL to the icon of the package. + /// An optional language of the package. + /// An optional URL to the license of the package. + /// An optional of the package. + /// An optional semicolon delimited list of owners of the package. + /// An optional containing the package types of the package. + /// An optional URL to the project of the package. + /// An optional value specifying release notes of the package. + /// An optional value specifying the type of source code repository of the package. + /// An optional value specifying the URL of the source code repository of the package. + /// An optional value specifying the branch of the source code repository of the package. + /// An optional value specifying the commit of the source code repository of the package. + /// An optional value indicating whether or not the package requires license acceptance The default value is false. + /// An option value indicating whether or not the package is serviceable. The default value is false. + /// An optional summary of the package. + /// An optional set of tags of the package. + /// An optional title of the package. + /// The current . +#endif + + public PackageRepository Package( + string name, + string version, + string authors = null, + string description = null, + string copyright = null, + bool developmentDependency = false, +#if !NET46 + string icon = null, +#endif + string iconUrl = null, + string language = null, + string licenseUrl = null, + LicenseMetadata licenseMetadata = null, + string owners = null, + IEnumerable packageTypes = null, + string projectUrl = null, + string releaseNotes = null, + string repositoryType = null, + string repositoryUrl = null, + string repositoryBranch = null, + string repositoryCommit = null, + bool requireLicenseAcceptance = false, + bool serviceable = false, + string summary = null, + string tags = null, + string title = null) + { + return Package( + name, + version, + out PackageIdentity _, + authors, + description, + copyright, + developmentDependency, +#if !NET46 + icon, +#endif + iconUrl, + language, + licenseUrl, + licenseMetadata, + owners, + packageTypes, + projectUrl, + releaseNotes, + repositoryType, + repositoryUrl, + repositoryBranch, + repositoryCommit, + requireLicenseAcceptance, + serviceable, + summary, + tags, + title); + } + +#if NET46 + /// + /// Creates a new package. + /// + /// The name or ID of the package. + /// The semantic version of the package. + /// Receives the of the package. + /// An optional semicolon delimited list of authors of the package. The default value is "UserA" + /// An optional description of the package. The default value is "Description" + /// An optional copyright of the package. + /// An optional value indicating whether or not the package is a development dependency. The default value is false. + /// An optional URL to the icon of the package. + /// An optional language of the package. + /// An optional URL to the license of the package. + /// An optional of the package. + /// An optional semicolon delimited list of owners of the package. + /// An optional containing the package types of the package. + /// An optional URL to the project of the package. + /// An optional value specifying release notes of the package. + /// An optional value specifying the type of source code repository of the package. + /// An optional value specifying the URL of the source code repository of the package. + /// An optional value specifying the branch of the source code repository of the package. + /// An optional value specifying the commit of the source code repository of the package. + /// An optional value indicating whether or not the package requires license acceptance The default value is false. + /// An option value indicating whether or not the package is serviceable. The default value is false. + /// An optional summary of the package. + /// An optional set of tags of the package. + /// An optional title of the package. + /// The current . +#else + /// + /// Creates a new package. + /// + /// The name or ID of the package. + /// The semantic version of the package. + /// Receives the of the package. + /// An optional semicolon delimited list of authors of the package. The default value is "UserA" + /// An optional description of the package. The default value is "Description" + /// An optional copyright of the package. + /// An optional value indicating whether or not the package is a development dependency. The default value is false. + /// An optional path in the package that should be used for the icon of the package. + /// An optional URL to the icon of the package. + /// An optional language of the package. + /// An optional URL to the license of the package. + /// An optional of the package. + /// An optional semicolon delimited list of owners of the package. + /// An optional containing the package types of the package. + /// An optional URL to the project of the package. + /// An optional value specifying release notes of the package. + /// An optional value specifying the type of source code repository of the package. + /// An optional value specifying the URL of the source code repository of the package. + /// An optional value specifying the branch of the source code repository of the package. + /// An optional value specifying the commit of the source code repository of the package. + /// An optional value indicating whether or not the package requires license acceptance The default value is false. + /// An option value indicating whether or not the package is serviceable. The default value is false. + /// An optional summary of the package. + /// An optional set of tags of the package. + /// An optional title of the package. + /// The current . +#endif + public PackageRepository Package( + string name, + string version, + out PackageIdentity package, + string authors = null, + string description = null, + string copyright = null, + bool developmentDependency = false, +#if !NET46 + string icon = null, +#endif + string iconUrl = null, + string language = null, + string licenseUrl = null, + LicenseMetadata licenseMetadata = null, + string owners = null, + IEnumerable packageTypes = null, + string projectUrl = null, + string releaseNotes = null, + string repositoryType = null, + string repositoryUrl = null, + string repositoryBranch = null, + string repositoryCommit = null, + bool requireLicenseAcceptance = false, + bool serviceable = false, + string summary = null, + string tags = null, + string title = null) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentNullException(nameof(name)); + } + + if (string.IsNullOrWhiteSpace(version)) + { + throw new ArgumentNullException(nameof(version)); + } + + package = new PackageIdentity(name, NuGetVersion.Parse(version)); + + string manifestFilePath = _versionFolderPathResolver.GetManifestFilePath(package.Id, package.Version); + + if (System.IO.File.Exists(manifestFilePath)) + { + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Strings.ErrorPackageAlreadyCreated, name, version)); + } + + _packageManifest = new PackageManifest( + manifestFilePath, + name, + version, + authors, + description, + copyright, + developmentDependency, +#if !NET46 + icon, +#endif + iconUrl, + language, + licenseUrl, + licenseMetadata, + owners, + packageTypes, + projectUrl, + releaseNotes, + repositoryType, + repositoryUrl, + repositoryBranch, + repositoryCommit, + requireLicenseAcceptance, + serviceable, + summary, + tags, + title); + + _packages.Add(package); + + return this; + } + } +} \ No newline at end of file diff --git a/src/MSBuildProjectCreator/PackageRepository.cs b/src/MSBuildProjectCreator/PackageRepository.cs new file mode 100644 index 0000000..1e5667a --- /dev/null +++ b/src/MSBuildProjectCreator/PackageRepository.cs @@ -0,0 +1,53 @@ +// Copyright (c) Jeff Kluge. All rights reserved. +// +// Licensed under the MIT license. + +using NuGet.Configuration; +using NuGet.Packaging; +using System.IO; +using System.Linq; + +namespace Microsoft.Build.Utilities.ProjectCreation +{ + /// + /// Represents a NuGet package repository. + /// + public partial class PackageRepository + { + private readonly VersionFolderPathResolver _versionFolderPathResolver; + + private PackageManifest _packageManifest; + + private PackageRepository(string rootPath) + { + GlobalPackagesFolder = Path.Combine(rootPath, ".nuget", SettingsUtility.DefaultGlobalPackagesFolderPath); + + ISettings settings = new Settings(rootPath, Settings.DefaultSettingsFileName); + + SettingsUtility.SetConfigValue(settings, ConfigurationConstants.GlobalPackagesFolder, GlobalPackagesFolder); + + settings.Remove(ConfigurationConstants.PackageSources, settings.GetSection(ConfigurationConstants.PackageSources).Items.First()); + + settings.AddOrUpdate(ConfigurationConstants.PackageSources, new ClearItem()); + + settings.SaveToDisk(); + + _versionFolderPathResolver = new VersionFolderPathResolver(GlobalPackagesFolder); + } + + /// + /// Gets the full path to the global packages folder. + /// + public string GlobalPackagesFolder { get; } + + /// + /// Creates a new instance. + /// + /// The root directory to create a package repository directory in. + /// A object that is used to construct an NuGet package repository. + public static PackageRepository Create(string rootPath) + { + return new PackageRepository(rootPath); + } + } +} \ No newline at end of file diff --git a/src/MSBuildProjectCreator/ProjectCreator.Items.cs b/src/MSBuildProjectCreator/ProjectCreator.Items.cs index d8a97d1..528a697 100644 --- a/src/MSBuildProjectCreator/ProjectCreator.Items.cs +++ b/src/MSBuildProjectCreator/ProjectCreator.Items.cs @@ -4,6 +4,7 @@ using Microsoft.Build.Construction; using Microsoft.Build.Evaluation; +using NuGet.Packaging.Core; using System; using System.Collections.Generic; using System.Linq; @@ -164,6 +165,21 @@ public ProjectCreator ItemPackageReference(string include, string version = null condition: condition); } + /// + /// Adds a <PackageReference /> item to the current item group. + /// + /// The of the package to reference. + /// An optional value specifying which assets belonging to the package should be consumed. + /// An optional value specifying which assets belonging to the package should be not consumed. + /// An optional value specifying which assets belonging to the package should not flow to dependent projects. + /// An optional containing metadata for the item. + /// An optional condition to add to the item. + /// The current . + public ProjectCreator ItemPackageReference(PackageIdentity package, string includeAssets = null, string excludeAssets = null, string privateAssets = null, IDictionary metadata = null, string condition = null) + { + return ItemPackageReference(package.Id, package.Version.ToNormalizedString(), includeAssets, excludeAssets, privateAssets, metadata, condition); + } + /// /// Adds a <ProjectReference /> item to the current item group. /// diff --git a/src/MSBuildProjectCreator/ProjectCreator.cs b/src/MSBuildProjectCreator/ProjectCreator.cs index 5cb1f49..fbe558a 100644 --- a/src/MSBuildProjectCreator/ProjectCreator.cs +++ b/src/MSBuildProjectCreator/ProjectCreator.cs @@ -5,6 +5,7 @@ using Microsoft.Build.Construction; using Microsoft.Build.Evaluation; using System; +using System.IO; namespace Microsoft.Build.Utilities.ProjectCreation { @@ -143,6 +144,8 @@ public ProjectCreator Save() /// The current . public ProjectCreator Save(string path) { + Directory.CreateDirectory(Path.GetDirectoryName(path)); + RootElement.Save(path); return this; diff --git a/src/MSBuildProjectCreator/Resources/Strings.Designer.cs b/src/MSBuildProjectCreator/Resources/Strings.Designer.cs index ef824ef..bf46a10 100644 --- a/src/MSBuildProjectCreator/Resources/Strings.Designer.cs +++ b/src/MSBuildProjectCreator/Resources/Strings.Designer.cs @@ -19,7 +19,7 @@ namespace Microsoft.Build.Utilities.ProjectCreation.Resources { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Strings { @@ -60,6 +60,24 @@ internal Strings() { } } + /// + /// Looks up a localized string similar to The file "{0}" has already been added to the package.. + /// + internal static string ErrorFileAlreadyCreated { + get { + return ResourceManager.GetString("ErrorFileAlreadyCreated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The specified file path "{0}" must have a parent directory.. + /// + internal static string ErrorFilePathMustBeInADirectory { + get { + return ResourceManager.GetString("ErrorFilePathMustBeInADirectory", resourceCulture); + } + } + /// /// Looks up a localized string similar to You can only add one Otherwise to a Choose.. /// @@ -78,6 +96,15 @@ internal static string ErrorOtherwiseRequresWhen { } } + /// + /// Looks up a localized string similar to The package "{0}" version "{1}" has already been created.. + /// + internal static string ErrorPackageAlreadyCreated { + get { + return ResourceManager.GetString("ErrorPackageAlreadyCreated", resourceCulture); + } + } + /// /// Looks up a localized string similar to You must add a Task by before you can add an output item.. /// @@ -123,6 +150,24 @@ internal static string ErrorUsingTaskParameterRequiresUsingTask { } } + /// + /// Looks up a localized string similar to You must add a package before adding build logic.. + /// + internal static string ErrorWhenAddingBuildLogicRequiresPackage { + get { + return ResourceManager.GetString("ErrorWhenAddingBuildLogicRequiresPackage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You must add a package before adding a library.. + /// + internal static string ErrorWhenAddingLibraryRequiresPackage { + get { + return ResourceManager.GetString("ErrorWhenAddingLibraryRequiresPackage", resourceCulture); + } + } + /// /// Looks up a localized string similar to You must add a When before adding a When ItemGroup.. /// diff --git a/src/MSBuildProjectCreator/Resources/Strings.resx b/src/MSBuildProjectCreator/Resources/Strings.resx index fe9b42e..883bbb4 100644 --- a/src/MSBuildProjectCreator/Resources/Strings.resx +++ b/src/MSBuildProjectCreator/Resources/Strings.resx @@ -117,12 +117,21 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + The file "{0}" has already been added to the package. + + + The specified file path "{0}" must have a parent directory. + You can only add one Otherwise to a Choose. You must add a When before adding an Otherwise. + + The package "{0}" version "{1}" has already been created. + You must add a Task by before you can add an output item. @@ -138,6 +147,12 @@ You must add a UsingTask before adding a UsingTask parameter. + + You must add a package before adding build logic. + + + You must add a package before adding a library. + You must add a When before adding a When ItemGroup. diff --git a/version.json b/version.json index 3bed904..930765a 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { - "version": "1.2", + "version": "1.3", "assemblyVersion": "1.0", "nugetPackageVersion": { "semVer": 1