Skip to content

Commit 8639b04

Browse files
Merge pull request #267 from reduckted/feature/publish-msbuild-target
MSBuild target for publishing to Visual Studio Marketplace
2 parents ac1741f + 5be8bae commit 8639b04

File tree

11 files changed

+963
-0
lines changed

11 files changed

+963
-0
lines changed

Community.VisualStudio.Toolkit.sln

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "imports", "imports", "{544D
7676
src\toolkit\nuget\build\imports\ExtensibilityEssentialsCheck.targets = src\toolkit\nuget\build\imports\ExtensibilityEssentialsCheck.targets
7777
src\toolkit\nuget\build\imports\MSBuildCapabilities.props = src\toolkit\nuget\build\imports\MSBuildCapabilities.props
7878
src\toolkit\nuget\build\imports\NewtonsoftJsonVersionCheck.targets = src\toolkit\nuget\build\imports\NewtonsoftJsonVersionCheck.targets
79+
src\toolkit\nuget\build\imports\PublishToMarketplace.targets = src\toolkit\nuget\build\imports\PublishToMarketplace.targets
7980
EndProjectSection
8081
EndProject
82+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MockVsixPublisher", "tools\MockVsixPublisher\MockVsixPublisher.csproj", "{C1BD4EBE-F43C-4A51-9801-A04E5C2C8B3E}"
83+
EndProject
8184
Global
8285
GlobalSection(SharedMSBuildProjectFiles) = preSolution
8386
src\toolkit\Community.VisualStudio.Toolkit.Shared\VSSDK.Helpers.Shared.projitems*{ff58bacd-16b0-4c73-ba03-4a255925153f}*SharedItemsImports = 5
@@ -153,6 +156,14 @@ Global
153156
{6AB57B05-4938-44BB-BBE4-38F3A55D1A54}.Release|Any CPU.Build.0 = Release|Any CPU
154157
{6AB57B05-4938-44BB-BBE4-38F3A55D1A54}.Release|x86.ActiveCfg = Release|Any CPU
155158
{6AB57B05-4938-44BB-BBE4-38F3A55D1A54}.Release|x86.Build.0 = Release|Any CPU
159+
{C1BD4EBE-F43C-4A51-9801-A04E5C2C8B3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
160+
{C1BD4EBE-F43C-4A51-9801-A04E5C2C8B3E}.Debug|Any CPU.Build.0 = Debug|Any CPU
161+
{C1BD4EBE-F43C-4A51-9801-A04E5C2C8B3E}.Debug|x86.ActiveCfg = Debug|Any CPU
162+
{C1BD4EBE-F43C-4A51-9801-A04E5C2C8B3E}.Debug|x86.Build.0 = Debug|Any CPU
163+
{C1BD4EBE-F43C-4A51-9801-A04E5C2C8B3E}.Release|Any CPU.ActiveCfg = Release|Any CPU
164+
{C1BD4EBE-F43C-4A51-9801-A04E5C2C8B3E}.Release|Any CPU.Build.0 = Release|Any CPU
165+
{C1BD4EBE-F43C-4A51-9801-A04E5C2C8B3E}.Release|x86.ActiveCfg = Release|Any CPU
166+
{C1BD4EBE-F43C-4A51-9801-A04E5C2C8B3E}.Release|x86.Build.0 = Release|Any CPU
156167
EndGlobalSection
157168
GlobalSection(SolutionProperties) = preSolution
158169
HideSolutionNode = FALSE
@@ -174,6 +185,7 @@ Global
174185
{2EC8051C-B422-4CEC-BA2F-BBD15551E6CB} = {6ED8D47D-D076-44C4-B0CE-B6CF944C71D3}
175186
{6ED8D47D-D076-44C4-B0CE-B6CF944C71D3} = {C27D922F-7897-4199-A2F9-6E1ED66C440A}
176187
{544DB8D0-6BAB-4739-B141-971AA59A494C} = {2EC8051C-B422-4CEC-BA2F-BBD15551E6CB}
188+
{C1BD4EBE-F43C-4A51-9801-A04E5C2C8B3E} = {3F2F4E99-C896-4C92-9F6D-66D2A8360168}
177189
EndGlobalSection
178190
GlobalSection(ExtensibilityGlobals) = postSolution
179191
SolutionGuid = {E0DD67C0-2617-498C-81F0-A5E4FF5831F1}

src/toolkit/nuget/build/Community.VisualStudio.Toolkit.targets

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22

33
<Import Project="$(MSBuildThisFileDirectory)imports\ExtensibilityEssentialsCheck.targets"/>
44
<Import Project="$(MSBuildThisFileDirectory)imports\NewtonsoftJsonVersionCheck.targets"/>
5+
<Import Project="$(MSBuildThisFileDirectory)imports\PublishToMarketplace.targets"/>
56

67
</Project>
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<Project>
2+
3+
<!--
4+
Provide a way for a build to be done prior to publishing, so
5+
that building and publishing can be done in a single step.
6+
-->
7+
<PropertyGroup Condition="'$(BuildBeforePublish)' == 'true'">
8+
<PublishToMarketplaceDependsOn>Rebuild</PublishToMarketplaceDependsOn>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<!--
13+
Update the build tools NuGet package reference to generate a path property. This will
14+
cause MSBuild to create a property called `PkgMicrosoft_VSSDK_BuildTools` that defines
15+
the location of the NuGet package. We will use that to find the VsixPublisher executable.
16+
-->
17+
<PackageReference Update="Microsoft.VSSDK.BuildTools">
18+
<GeneratePathProperty>true</GeneratePathProperty>
19+
</PackageReference>
20+
</ItemGroup>
21+
22+
<Target Name="PublishToMarketplace" DependsOnTargets="$(PublishToMarketplaceDependsOn)">
23+
<Message Text="Publishing extension to the marketplace..." Importance="normal"/>
24+
25+
<!-- Before anything else happens, ensure that only "Release" builds can be published. -->
26+
<Error
27+
Condition="'$(Configuration)' != 'Release'"
28+
Text="The configuration must be 'Release' when publishing to the marketplace."
29+
/>
30+
31+
<PropertyGroup>
32+
<!--
33+
Find the VsixPublisher executable if it has not been specified. This uses the property that was
34+
generated by setting the `GeneratePathProperty` property to true for the build tools NuGet package.
35+
-->
36+
<VsixPublisher Condition="'$(VsixPublisher)' == ''">$(PkgMicrosoft_VSSDK_BuildTools)\tools\vssdk\bin\VsixPublisher.exe</VsixPublisher>
37+
38+
<!--
39+
If a manifest file name has not been specified, then search for it by starting from the
40+
project directory and looking up. Although the function is called `GetPathOfFileAbove`,
41+
it actually starts looking in the specified directory (the second parameter), so the
42+
manifest file can be placed alongside the the project file and it will be found.
43+
-->
44+
<PublishManifest Condition="'$(PublishManifest)' == ''">$([MSBuild]::GetPathOfFileAbove('publish.json', '$(ProjectDir)'))</PublishManifest>
45+
46+
<!-- Build the path to the extension file if it hasn't been specified. -->
47+
<PublishExtension Condition="'$(PublishExtension)' == ''">$(ProjectDir)$(TargetVsixContainer)</PublishExtension>
48+
</PropertyGroup>
49+
50+
<!-- Log some properties to assist with debugging. -->
51+
<Message Text="BuildTools: $(PkgMicrosoft_VSSDK_BuildTools)" Importance="$(PublishLogLevel)" />
52+
<Message Text="VsixPublisher: $(VsixPublisher)" Importance="$(PublishLogLevel)" />
53+
<Message Text="Manifest: $(PublishManifest)" Importance="$(PublishLogLevel)" />
54+
<Message Text="Extension: $(PublishExtension)" Importance="$(PublishLogLevel)" />
55+
<Message Text="Ignore Warnings: $(PublishIgnoreWarnings)" Importance="$(PublishLogLevel)" />
56+
57+
<!-- Verify that the manifest file was found. -->
58+
<Error
59+
Condition="'$(PublishManifest)' == '' or !Exists('$(PublishManifest)')"
60+
Text="The 'publish manifest' file was not found. Either specify the 'PublishManifest' build property, or create a 'publish.json' file in the same directory as the project file or solution file. For more information about the 'publish manifest' file, visit https://docs.microsoft.com/visualstudio/extensibility/walkthrough-publishing-a-visual-studio-extension-via-command-line#publishmanifest-file"
61+
/>
62+
63+
<!-- A personal access token needs to be specified. -->
64+
<Error
65+
Condition="'$(PersonalAccessToken)' == ''"
66+
Text="A personal access token must be specified in the 'PersonalAccessToken' build property."
67+
/>
68+
69+
<!-- Verify that the extension file was found. -->
70+
<Error
71+
Condition="'$(PublishExtension)' == '' or !Exists('$(PublishExtension)')"
72+
Text="The extension file could not be found at '$(PublishExtension)'."
73+
/>
74+
75+
<!-- Everything should be valid now, so define the command to publish the extension. -->
76+
<PropertyGroup>
77+
<PublishCommand>&quot;$(VsixPublisher)&quot; publish -personalAccessToken &quot;$(PersonalAccessToken)&quot; -payload &quot;$(PublishExtension)&quot; -publishManifest &quot;$(PublishManifest)&quot;</PublishCommand>
78+
<PublishCommand Condition="'$(PublishIgnoreWarnings)' != ''">$(PublishCommand) -ignoreWarnings &quot;$(PublishIgnoreWarnings)&quot;</PublishCommand>
79+
</PropertyGroup>
80+
81+
<Exec
82+
Command="$(PublishCommand)"
83+
StandardOutputImportance="normal"
84+
StandardErrorImportance="high"
85+
LogStandardErrorAsError="true"
86+
IgnoreExitCode="true"
87+
>
88+
<Output TaskParameter="ExitCode" PropertyName="PublishExitCode"/>
89+
</Exec>
90+
91+
<Message Condition="'$(PublishExitCode)' == '0'" Text="Extension published successfully." Importance="normal"/>
92+
<Error Condition="'$(PublishExitCode)' != '0'" Text="Failed to publish the extension."/>
93+
</Target>
94+
95+
</Project>

test/toolkit/Community.VisualStudio.Toolkit.UnitTests/Community.VisualStudio.Toolkit.UnitTests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<Import Project="..\..\..\src\toolkit\Community.VisualStudio.Toolkit.Shared\VSSDK.Helpers.Shared.projitems" Label="Shared" />
1717

1818
<ItemGroup>
19+
<PackageReference Include="Microsoft.Build.Locator" Version="1.4.1" />
1920
<PackageReference Include="Microsoft.VisualStudio.SDK" Version="17.0.31902.203" IncludeAssets="All" PrivateAssets="None" />
2021
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0" />
2122
<PackageReference Include="Moq" Version="4.16.1" />
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System.Collections.Generic;
2+
3+
namespace Community.VisualStudio.Toolkit.UnitTests
4+
{
5+
internal class CompilationResult
6+
{
7+
public CompilationResult(int exitCode, IEnumerable<string> standardOutput, IEnumerable<string> standardError)
8+
{
9+
ExitCode = exitCode;
10+
StandardOutput = standardOutput;
11+
StandardError = standardError;
12+
}
13+
14+
public int ExitCode { get; }
15+
16+
public IEnumerable<string> StandardOutput { get; }
17+
18+
public IEnumerable<string> StandardError { get; }
19+
}
20+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
4+
namespace Community.VisualStudio.Toolkit.UnitTests
5+
{
6+
public class CompileOptions
7+
{
8+
public string Target { get; set; } = "Build";
9+
public object Properties { get; set; } = new object();
10+
public IEnumerable<string> Arguments { get; set; } = Enumerable.Empty<string>();
11+
public Dictionary<string, string> Environment { get; set; } = new Dictionary<string, string>();
12+
}
13+
}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.IO;
5+
using System.Reflection;
6+
using System.Runtime.CompilerServices;
7+
using System.Text;
8+
using System.Threading.Tasks;
9+
using Microsoft.Build.Locator;
10+
using Xunit.Abstractions;
11+
12+
namespace Community.VisualStudio.Toolkit.UnitTests
13+
{
14+
internal class TestProject
15+
{
16+
private static readonly VisualStudioInstance _instance = MSBuildLocator.RegisterDefaults();
17+
18+
private readonly string _directory;
19+
private readonly List<string> _files = new();
20+
private readonly List<string> _propsImports = new();
21+
private readonly List<string> _targetsImports = new();
22+
private readonly List<string> _targetsElements = new();
23+
24+
public TestProject(string directory)
25+
{
26+
_directory = directory;
27+
}
28+
29+
public void ImportTargets(string fileName)
30+
{
31+
string path = GetTargetsPath(fileName);
32+
_targetsImports.Add($"<Import Project='{path}' />");
33+
}
34+
35+
private static string GetTargetsPath(string fileName, [CallerFilePath] string thisFilePath = "")
36+
{
37+
return Path.GetFullPath(
38+
Path.Combine(
39+
Path.GetDirectoryName(thisFilePath),
40+
$"../../../../src/toolkit/nuget/build/{fileName}"
41+
)
42+
);
43+
}
44+
45+
public void AddFile(string fileName, string contents)
46+
{
47+
string fullPath = Path.Combine(_directory, fileName);
48+
Directory.CreateDirectory(Path.GetDirectoryName(fullPath));
49+
File.WriteAllText(fullPath, contents);
50+
51+
_files.Add($"<Compile Include='{fileName}'/>");
52+
}
53+
54+
public void AddTargetElement(string element)
55+
{
56+
_targetsElements.Add(element);
57+
}
58+
59+
public async Task<CompilationResult> CompileAsync(CompileOptions options, ITestOutputHelper outputHelper)
60+
{
61+
WriteFiles();
62+
63+
using (Process process = new())
64+
{
65+
List<string> stdout = new();
66+
List<string> stderr = new();
67+
List<string> arguments = new();
68+
69+
arguments.Add("/Restore");
70+
arguments.Add($"/t:{options.Target}");
71+
arguments.Add("/nr:false"); // Disable node re-use.
72+
73+
foreach (PropertyInfo property in options.Properties.GetType().GetProperties())
74+
{
75+
object value = property.GetValue(options.Properties);
76+
if (value is not null)
77+
{
78+
arguments.Add($"/p:{property.Name}={value}");
79+
}
80+
}
81+
82+
foreach (string argument in options.Arguments)
83+
{
84+
arguments.Add(argument);
85+
}
86+
87+
process.StartInfo = new ProcessStartInfo
88+
{
89+
FileName = Path.Combine(_instance.MSBuildPath, "MSBuild.exe"),
90+
Arguments = string.Join(" ", arguments),
91+
WorkingDirectory = _directory,
92+
UseShellExecute = false,
93+
RedirectStandardOutput = true,
94+
RedirectStandardError = true,
95+
StandardOutputEncoding = Encoding.UTF8,
96+
StandardErrorEncoding = Encoding.UTF8
97+
};
98+
99+
foreach (KeyValuePair<string, string> entry in options.Environment)
100+
{
101+
process.StartInfo.Environment[entry.Key] = entry.Value;
102+
}
103+
104+
process.Start();
105+
106+
await Task.WhenAll(
107+
DrainReaderAsync(process.StandardOutput, (line) =>
108+
{
109+
stdout.Add(line);
110+
outputHelper.WriteLine(line);
111+
}),
112+
DrainReaderAsync(process.StandardError, (line) =>
113+
{
114+
stderr.Add(line);
115+
outputHelper.WriteLine(line);
116+
})
117+
);
118+
119+
process.WaitForExit();
120+
121+
return new CompilationResult(process.ExitCode, stdout, stderr);
122+
}
123+
}
124+
125+
private void WriteFiles()
126+
{
127+
WriteManifest();
128+
WriteProject();
129+
}
130+
131+
private void WriteProject()
132+
{
133+
string projectName = $"{Path.GetFileName(_directory)}.csproj";
134+
string projectFileName = Path.Combine(_directory, projectName);
135+
136+
File.WriteAllText(
137+
projectFileName,
138+
$@"
139+
<Project ToolsVersion='17.0' DefaultTargets='Build' xmlns='http://schemas.microsoft.com/developer/msbuild/2003'>
140+
<PropertyGroup>
141+
<VSToolsPath Condition=""'$(VSToolsPath)' == ''"">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
142+
</PropertyGroup>
143+
<Import Project='$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props' Condition=""Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')"" />
144+
{string.Join(Environment.NewLine, _propsImports)}
145+
<PropertyGroup>
146+
<Configuration Condition="" '$(Configuration)' == '' "">Debug</Configuration>
147+
<Platform Condition="" '$(Platform)' == '' "">AnyCPU</Platform>
148+
<ProjectTypeGuids>{{82b43b9b-a64c-4715-b499-d71e9ca2bd60}};{{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}}</ProjectTypeGuids>
149+
<ProjectGuid>{{C313D707-2A74-4AD2-BB5D-0FD6C4942E08}}</ProjectGuid>
150+
<OutputType>Library</OutputType>
151+
<RootNamespace>Test.Extension</RootNamespace>
152+
<AssemblyName>Extension</AssemblyName>
153+
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
154+
<OutputPath>bin\$(Configuration)</OutputPath>
155+
<GeneratePkgDefFile>false</GeneratePkgDefFile>
156+
<DeployExtension>False</DeployExtension>
157+
</PropertyGroup>
158+
<ItemGroup>
159+
<None Include='source.extension.vsixmanifest'/>
160+
</ItemGroup>
161+
<ItemGroup>
162+
{string.Join(Environment.NewLine, _files)}
163+
</ItemGroup>
164+
<ItemGroup>
165+
<PackageReference Include='Microsoft.VSSDK.BuildTools' Version='17.0.5232'>
166+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
167+
<PrivateAssets>all</PrivateAssets>
168+
</PackageReference>
169+
</ItemGroup>
170+
{string.Join(Environment.NewLine, _targetsElements)}
171+
{string.Join(Environment.NewLine, _targetsImports)}
172+
<Import Project='$(MSBuildToolsPath)\Microsoft.CSharp.targets' />
173+
<Import Project='$(VSToolsPath)\VSSDK\Microsoft.VsSDK.targets' Condition=""'$(VSToolsPath)' != ''"" />
174+
</Project>");
175+
}
176+
177+
private void WriteManifest()
178+
{
179+
File.WriteAllText(
180+
Path.Combine(_directory, "source.extension.vsixmanifest"),
181+
@"<?xml version='1.0' encoding='utf-8'?>
182+
<PackageManifest Version='2.0.0' xmlns='http://schemas.microsoft.com/developer/vsx-schema/2011' xmlns:d='http://schemas.microsoft.com/developer/vsx-schema-design/2011'>
183+
<Metadata>
184+
<Identity Id='VSSDK.TestExtension.5a9a059d-5738-41dc-9075-250890b4ef6f' Version='1.0' Language='en-US' Publisher='Mads Kristensen' />
185+
<DisplayName>VSSDK.TestExtension</DisplayName>
186+
<Description>Empty VSIX Project.</Description>
187+
</Metadata>
188+
<Installation>
189+
<InstallationTarget Id='Microsoft.VisualStudio.Community' Version='[16.0, 17.0)' />
190+
</Installation>
191+
<Dependencies>
192+
<Dependency Id='Microsoft.Framework.NDP' DisplayName='Microsoft .NET Framework' d:Source='Manual' Version='[4.5,)' />
193+
</Dependencies>
194+
<Prerequisites>
195+
<Prerequisite Id='Microsoft.VisualStudio.Component.CoreEditor' Version='[16.0,17.0)' DisplayName='Visual Studio core editor' />
196+
</Prerequisites>
197+
<Assets>
198+
<Asset Type='Microsoft.VisualStudio.VsPackage' d:Source='Project' d:ProjectName='%CurrentProject%' Path='|%CurrentProject%;PkgdefProjectOutputGroup|' />
199+
<Asset Type='Microsoft.VisualStudio.MefComponent' d:Source='Project' d:ProjectName='%CurrentProject%' Path='|%CurrentProject%|' />
200+
</Assets>
201+
</PackageManifest>");
202+
}
203+
204+
private static async Task DrainReaderAsync(StreamReader reader, Action<string> output)
205+
{
206+
string line;
207+
while ((line = await reader.ReadLineAsync()) is not null)
208+
{
209+
output(line);
210+
}
211+
}
212+
}
213+
}

0 commit comments

Comments
 (0)