second)
+ {
+ int count = 0;
+ int minLength = Math.Min(first.Length, second.Length);
+ for (int i = 0; i < minLength; i++)
+ {
+ if (first[i] == second[i])
+ {
+ count++;
+ }
+ else
+ {
+ break;
+ }
+ }
+
+ return count;
+ }
+ }
+}
diff --git a/apidocs/ScrapeDocs/Program.cs b/apidocs/ScrapeDocs/Program.cs
new file mode 100644
index 00000000..6e4251ae
--- /dev/null
+++ b/apidocs/ScrapeDocs/Program.cs
@@ -0,0 +1,643 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for full license information.
+
+namespace ScrapeDocs
+{
+ using System;
+ using System.Collections.Concurrent;
+ using System.Collections.Generic;
+ using System.Diagnostics;
+ using System.Diagnostics.CodeAnalysis;
+ using System.Globalization;
+ using System.IO;
+ using System.Linq;
+ using System.Reflection;
+ using System.Text;
+ using System.Text.Json;
+ using System.Text.RegularExpressions;
+ using System.Threading;
+ using MessagePack;
+ using Microsoft.Windows.SDK.Win32Docs;
+ using YamlDotNet;
+ using YamlDotNet.RepresentationModel;
+
+ ///
+ /// Program entrypoint class.
+ ///
+ internal class Program
+ {
+ private static readonly Regex FileNamePattern = new Regex(@"^\w\w-\w+-([\w\-]+)$", RegexOptions.Compiled);
+ private static readonly Regex ParameterHeaderPattern = new Regex(@"^### -param (\w+)", RegexOptions.Compiled);
+ private static readonly Regex FieldHeaderPattern = new Regex(@"^### -field (?:\w+\.)*(\w+)", RegexOptions.Compiled);
+ private static readonly Regex ReturnHeaderPattern = new Regex(@"^## -returns", RegexOptions.Compiled);
+ private static readonly Regex RemarksHeaderPattern = new Regex(@"^## -remarks", RegexOptions.Compiled);
+ private static readonly Regex InlineCodeTag = new Regex(@"\(.*)\
", RegexOptions.Compiled);
+ private static readonly Regex EnumNameCell = new Regex(@"\]*\>\([\dxa-f]+)\<\/dt\>", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+ private readonly string contentBasePath;
+ private readonly string outputPath;
+
+ private Program(string contentBasePath, string outputPath)
+ {
+ this.contentBasePath = contentBasePath;
+ this.outputPath = outputPath;
+ }
+
+ private bool EmitEnums { get; set; }
+
+ private static int Main(string[] args)
+ {
+ using var cts = new CancellationTokenSource();
+ Console.CancelKeyPress += (s, e) =>
+ {
+ Console.WriteLine("Canceling...");
+ cts.Cancel();
+ e.Cancel = true;
+ };
+
+ if (args.Length < 2)
+ {
+ Console.Error.WriteLine("USAGE: {0} [enums]");
+ return 1;
+ }
+
+ string contentBasePath = args[0];
+ string outputPath = args[1];
+ bool emitEnums = args.Length > 2 ? args[2] == "enums" : false;
+
+ try
+ {
+ new Program(contentBasePath, outputPath) { EmitEnums = true }.Worker(cts.Token);
+ }
+ catch (OperationCanceledException ex) when (ex.CancellationToken == cts.Token)
+ {
+ return 2;
+ }
+
+ return 0;
+ }
+
+ private static void Expect(string? expected, string? actual)
+ {
+ if (expected != actual)
+ {
+ throw new InvalidOperationException($"Expected: \"{expected}\" but read: \"{actual}\".");
+ }
+ }
+
+ private int AnalyzeEnums(ConcurrentDictionary results, ConcurrentDictionary<(string MethodName, string ParameterName, string HelpLink), DocEnum> parameterEnums, ConcurrentDictionary<(string MethodName, string ParameterName, string HelpLink), DocEnum> fieldEnums)
+ {
+ var uniqueEnums = new Dictionary>();
+ var constantsDocs = new Dictionary>();
+
+ void Collect(ConcurrentDictionary<(string MethodName, string ParameterName, string HelpLink), DocEnum> enums, bool isMethod)
+ {
+ foreach (var item in enums)
+ {
+ if (!uniqueEnums.TryGetValue(item.Value, out List<(string MethodName, string ParameterName, string HelpLink, bool IsMethod)>? list))
+ {
+ uniqueEnums.Add(item.Value, list = new());
+ }
+
+ list.Add((item.Key.MethodName, item.Key.ParameterName, item.Key.HelpLink, isMethod));
+
+ foreach (KeyValuePair enumValue in item.Value.Members)
+ {
+ if (enumValue.Value.Doc is object)
+ {
+ if (!constantsDocs.TryGetValue(enumValue.Key, out List<(string MethodName, string HelpLink, string Doc)>? values))
+ {
+ constantsDocs.Add(enumValue.Key, values = new());
+ }
+
+ values.Add((item.Key.MethodName, item.Key.HelpLink, enumValue.Value.Doc));
+ }
+ }
+ }
+ }
+
+ Collect(parameterEnums, isMethod: true);
+ Collect(fieldEnums, isMethod: false);
+
+ foreach (var item in constantsDocs)
+ {
+ var docNode = new ApiDetails();
+ docNode.Description = item.Value[0].Doc;
+
+ // If the documentation varies across methods, just link to each document.
+ bool differenceDetected = false;
+ for (int i = 1; i < item.Value.Count; i++)
+ {
+ if (item.Value[i].Doc != docNode.Description)
+ {
+ differenceDetected = true;
+ break;
+ }
+ }
+
+ if (differenceDetected)
+ {
+ docNode.Description = "Documentation varies per use. Refer to each: " + string.Join(", ", item.Value.Select(v => @$"{v.MethodOrStructName}")) + ".";
+ }
+ else
+ {
+ // Just point to any arbitrary method that documents it.
+ docNode.HelpLink = new Uri(item.Value[0].HelpLink);
+ }
+
+ results.TryAdd(item.Key, docNode);
+ }
+
+ if (this.EmitEnums)
+ {
+ string enumDirectory = Path.GetDirectoryName(this.outputPath) ?? throw new InvalidOperationException("Unable to determine where to write enums.");
+ Directory.CreateDirectory(enumDirectory);
+ using var enumsJsonStream = File.OpenWrite(Path.Combine(enumDirectory, "enums.json"));
+ using var writer = new Utf8JsonWriter(enumsJsonStream, new JsonWriterOptions { Indented = true });
+ writer.WriteStartArray();
+
+ foreach (KeyValuePair> item in uniqueEnums)
+ {
+ writer.WriteStartObject();
+
+ if (item.Key.GetRecommendedName(item.Value) is string enumName)
+ {
+ writer.WriteString("name", enumName);
+ }
+
+ writer.WriteBoolean("flags", item.Key.IsFlags);
+
+ writer.WritePropertyName("members");
+ writer.WriteStartArray();
+ foreach (var member in item.Key.Members)
+ {
+ writer.WriteStartObject();
+ writer.WriteString("name", member.Key);
+ if (member.Value.Value is ulong value)
+ {
+ writer.WriteString("value", value.ToString(CultureInfo.InvariantCulture));
+ }
+
+ writer.WriteEndObject();
+ }
+
+ writer.WriteEndArray();
+
+ writer.WritePropertyName("uses");
+ writer.WriteStartArray();
+ foreach (var uses in item.Value)
+ {
+ writer.WriteStartObject();
+
+ int periodIndex = uses.MethodName.IndexOf('.', StringComparison.Ordinal);
+ string? iface = periodIndex >= 0 ? uses.MethodName.Substring(0, periodIndex) : null;
+ string name = periodIndex >= 0 ? uses.MethodName.Substring(periodIndex + 1) : uses.MethodName;
+
+ if (iface is string)
+ {
+ writer.WriteString("interface", iface);
+ }
+
+ writer.WriteString(uses.IsMethod ? "method" : "struct", name);
+ writer.WriteString(uses.IsMethod ? "parameter" : "field", uses.ParameterName);
+
+ writer.WriteEndObject();
+ }
+
+ writer.WriteEndArray();
+ writer.WriteEndObject();
+ }
+
+ writer.WriteEndArray();
+ }
+
+ return constantsDocs.Count;
+ }
+
+ private void Worker(CancellationToken cancellationToken)
+ {
+ Console.WriteLine("Enumerating documents to be parsed...");
+ string[] paths = Directory.GetFiles(this.contentBasePath, "??-*-*.md", SearchOption.AllDirectories)
+ ////.Where(p => p.Contains(@"ns-winsock2-blob", StringComparison.OrdinalIgnoreCase)).ToArray()
+ ;
+
+ Console.WriteLine("Parsing documents...");
+ var timer = Stopwatch.StartNew();
+ var parsedNodes = from path in paths.AsParallel()
+ let result = this.ParseDocFile(path)
+ where result is not null
+ select (Path: path, result.Value.ApiName, result.Value.Docs, result.Value.EnumsByParameter, result.Value.EnumsByField);
+ var results = new ConcurrentDictionary();
+ var parameterEnums = new ConcurrentDictionary<(string MethodName, string ParameterName, string HelpLink), DocEnum>();
+ var fieldEnums = new ConcurrentDictionary<(string StructName, string FieldName, string HelpLink), DocEnum>();
+ if (Debugger.IsAttached)
+ {
+ parsedNodes = parsedNodes.WithDegreeOfParallelism(1); // improve debuggability
+ }
+
+ parsedNodes
+ .WithCancellation<(string Path, string ApiName, ApiDetails Docs, IReadOnlyDictionary EnumsByParameter, IReadOnlyDictionary EnumsByField)>(cancellationToken)
+ .ForAll(result =>
+ {
+ results.TryAdd(result.ApiName, result.Docs);
+ foreach (var e in result.EnumsByParameter)
+ {
+ if (result.Docs.HelpLink is object)
+ {
+ parameterEnums.TryAdd((result.ApiName, e.Key, result.Docs.HelpLink.AbsoluteUri), e.Value);
+ }
+ }
+
+ foreach (var e in result.EnumsByField)
+ {
+ if (result.Docs.HelpLink is object)
+ {
+ fieldEnums.TryAdd((result.ApiName, e.Key, result.Docs.HelpLink.AbsoluteUri), e.Value);
+ }
+ }
+ });
+ if (paths.Length == 0)
+ {
+ Console.Error.WriteLine("No documents found to parse.");
+ }
+ else
+ {
+ Console.WriteLine("Parsed {2} documents in {0} ({1} per document)", timer.Elapsed, timer.Elapsed / paths.Length, paths.Length);
+ Console.WriteLine($"Found {parameterEnums.Count + fieldEnums.Count} enums.");
+ }
+
+ Console.WriteLine("Analyzing and naming enums and collecting docs on their members...");
+ int constantsCount = this.AnalyzeEnums(results, parameterEnums, fieldEnums);
+ Console.WriteLine($"Found docs for {constantsCount} constants.");
+
+ Console.WriteLine("Writing results to \"{0}\"", this.outputPath);
+ Directory.CreateDirectory(Path.GetDirectoryName(this.outputPath)!);
+ using var outputFileStream = File.OpenWrite(this.outputPath);
+ MessagePackSerializer.Serialize(outputFileStream, results.ToDictionary(kv => kv.Key, kv => kv.Value), MessagePackSerializerOptions.Standard);
+ }
+
+ private (string ApiName, ApiDetails Docs, IReadOnlyDictionary EnumsByParameter, IReadOnlyDictionary EnumsByField)? ParseDocFile(string filePath)
+ {
+ try
+ {
+ var enumsByParameter = new Dictionary();
+ var enumsByField = new Dictionary();
+ var yaml = new YamlStream();
+ using StreamReader mdFileReader = File.OpenText(filePath);
+ using var markdownToYamlReader = new YamlSectionReader(mdFileReader);
+ var yamlBuilder = new StringBuilder();
+ ApiDetails docs = new();
+ string? line;
+ while ((line = markdownToYamlReader.ReadLine()) is object)
+ {
+ yamlBuilder.AppendLine(line);
+ }
+
+ try
+ {
+ yaml.Load(new StringReader(yamlBuilder.ToString()));
+ }
+ catch (YamlDotNet.Core.YamlException ex)
+ {
+ Debug.WriteLine("YAML parsing error in \"{0}\": {1}", filePath, ex.Message);
+ return null;
+ }
+
+ YamlSequenceNode methodNames = (YamlSequenceNode)yaml.Documents[0].RootNode["api_name"];
+ bool TryGetProperName(string searchFor, string? suffix, [NotNullWhen(true)] out string? match)
+ {
+ if (suffix is string)
+ {
+ if (searchFor.EndsWith(suffix, StringComparison.Ordinal))
+ {
+ searchFor = searchFor.Substring(0, searchFor.Length - suffix.Length);
+ }
+ else
+ {
+ match = null;
+ return false;
+ }
+ }
+
+ match = methodNames.Children.Cast().FirstOrDefault(c => string.Equals(c.Value?.Replace('.', '-'), searchFor, StringComparison.OrdinalIgnoreCase))?.Value;
+
+ if (suffix is string && match is object)
+ {
+ match += suffix.ToUpper(CultureInfo.InvariantCulture);
+ }
+
+ return match is object;
+ }
+
+ string presumedMethodName = FileNamePattern.Match(Path.GetFileNameWithoutExtension(filePath)).Groups[1].Value;
+
+ // Some structures have filenames that include the W or A suffix when the content doesn't. So try some fuzzy matching.
+ if (!TryGetProperName(presumedMethodName, null, out string? properName) &&
+ !TryGetProperName(presumedMethodName, "a", out properName) &&
+ !TryGetProperName(presumedMethodName, "w", out properName) &&
+ !TryGetProperName(presumedMethodName, "32", out properName) &&
+ !TryGetProperName(presumedMethodName, "64", out properName))
+ {
+ Debug.WriteLine("WARNING: Could not find proper API name in: {0}", filePath);
+ return null;
+ }
+
+ Uri helpLink = new Uri("https://docs.microsoft.com/windows/win32/api/" + filePath.Substring(this.contentBasePath.Length, filePath.Length - 3 - this.contentBasePath.Length).Replace('\\', '/'));
+ docs.HelpLink = helpLink;
+
+ var description = ((YamlMappingNode)yaml.Documents[0].RootNode).Children.FirstOrDefault(n => n.Key is YamlScalarNode { Value: "description" }).Value as YamlScalarNode;
+ docs.Description = description?.Value;
+
+ // Search for parameter/field docs
+ var parametersMap = new YamlMappingNode();
+ var fieldsMap = new YamlMappingNode();
+ StringBuilder docBuilder = new StringBuilder();
+ line = mdFileReader.ReadLine();
+
+ static string FixupLine(string line)
+ {
+ line = line.Replace("href=\"/", "href=\"https://docs.microsoft.com/");
+ line = InlineCodeTag.Replace(line, match => $"{match.Groups[1].Value}");
+ return line;
+ }
+
+ void ParseTextSection(out string text)
+ {
+ while ((line = mdFileReader.ReadLine()) is object)
+ {
+ if (line.StartsWith('#'))
+ {
+ break;
+ }
+
+ line = FixupLine(line);
+ docBuilder.AppendLine(line);
+ }
+
+ text = docBuilder.ToString();
+
+ docBuilder.Clear();
+ }
+
+ IReadOnlyDictionary ParseEnumTable()
+ {
+ var enums = new Dictionary();
+ int state = 0;
+ const int StateReadingHeader = 0;
+ const int StateReadingName = 1;
+ const int StateLookingForDetail = 2;
+ const int StateReadingDocColumn = 3;
+ string? enumName = null;
+ ulong? enumValue = null;
+ var docsBuilder = new StringBuilder();
+ while ((line = mdFileReader.ReadLine()) is object)
+ {
+ if (line == "")
+ {
+ break;
+ }
+
+ switch (state)
+ {
+ case StateReadingHeader:
+ // Reading TR header
+ if (line == "")
+ {
+ state = StateReadingName;
+ }
+
+ break;
+
+ case StateReadingName:
+ // Reading an enum row's name column.
+ Match m = EnumNameCell.Match(line);
+ if (m.Success)
+ {
+ enumName = m.Groups[1].Value;
+ if (enumName == "0")
+ {
+ enumName = "None";
+ enumValue = 0;
+ }
+
+ state = StateLookingForDetail;
+ }
+
+ break;
+
+ case StateLookingForDetail:
+ // Looking for an enum row's doc column.
+ m = EnumOrdinalValue.Match(line);
+ if (m.Success)
+ {
+ string value = m.Groups[1].Value;
+ bool hex = value.StartsWith("0x", StringComparison.OrdinalIgnoreCase);
+ if (hex)
+ {
+ value = value.Substring(2);
+ }
+
+ enumValue = ulong.Parse(value, hex ? NumberStyles.HexNumber : NumberStyles.Integer, CultureInfo.InvariantCulture);
+ }
+ else if (line.StartsWith("", StringComparison.OrdinalIgnoreCase))
+ {
+ // The row ended before we found the doc column.
+ state = StateReadingName;
+ enums.Add(enumName!, (enumValue, null));
+ enumName = null;
+ enumValue = null;
+ }
+
+ break;
+
+ case StateReadingDocColumn:
+ // Reading the enum row's doc column.
+ if (line.StartsWith(" | ", StringComparison.OrdinalIgnoreCase))
+ {
+ state = StateReadingName;
+
+ // Some docs are invalid in documenting the same enum multiple times.
+ if (!enums.ContainsKey(enumName!))
+ {
+ enums.Add(enumName!, (enumValue, docsBuilder.ToString().Trim()));
+ }
+
+ enumName = null;
+ enumValue = null;
+ docsBuilder.Clear();
+ break;
+ }
+
+ docsBuilder.AppendLine(FixupLine(line));
+ break;
+ }
+ }
+
+ return enums;
+ }
+
+ void ParseSection(Match match, IDictionary receivingMap, bool lookForParameterEnums = false, bool lookForFieldEnums = false)
+ {
+ string sectionName = match.Groups[1].Value;
+ bool foundEnum = false;
+ bool foundEnumIsFlags = false;
+ while ((line = mdFileReader.ReadLine()) is object)
+ {
+ if (line.StartsWith('#'))
+ {
+ break;
+ }
+
+ if (lookForParameterEnums || lookForFieldEnums)
+ {
+ if (foundEnum)
+ {
+ if (line == "")
+ {
+ IReadOnlyDictionary enumNamesAndDocs = ParseEnumTable();
+ if (enumNamesAndDocs.Count > 0)
+ {
+ var enums = lookForParameterEnums ? enumsByParameter : enumsByField;
+ if (!enums.ContainsKey(sectionName))
+ {
+ enums.Add(sectionName, new DocEnum(foundEnumIsFlags, enumNamesAndDocs));
+ }
+ }
+
+ lookForParameterEnums = false;
+ lookForFieldEnums = false;
+ }
+ }
+ else
+ {
+ foundEnum = line.Contains("of the following values", StringComparison.OrdinalIgnoreCase);
+ foundEnumIsFlags = line.Contains("combination of", StringComparison.OrdinalIgnoreCase)
+ || line.Contains("zero or more of", StringComparison.OrdinalIgnoreCase)
+ || line.Contains("one or both of", StringComparison.OrdinalIgnoreCase)
+ || line.Contains("one or more of", StringComparison.OrdinalIgnoreCase);
+ }
+ }
+
+ if (!foundEnum)
+ {
+ line = FixupLine(line);
+ docBuilder.AppendLine(line);
+ }
+ }
+
+ receivingMap.TryAdd(sectionName, docBuilder.ToString().Trim());
+ docBuilder.Clear();
+ }
+
+ while (line is object)
+ {
+ if (ParameterHeaderPattern.Match(line) is Match { Success: true } parameterMatch)
+ {
+ ParseSection(parameterMatch, docs.Parameters, lookForParameterEnums: true);
+ }
+ else if (FieldHeaderPattern.Match(line) is Match { Success: true } fieldMatch)
+ {
+ ParseSection(fieldMatch, docs.Fields, lookForFieldEnums: true);
+ }
+ else if (RemarksHeaderPattern.Match(line) is Match { Success: true } remarksMatch)
+ {
+ string remarks;
+ ParseTextSection(out remarks);
+ docs.Remarks = remarks;
+ }
+ else
+ {
+ // TODO: don't break out of this loop so soon... remarks sometimes follows return value docs.
+ if (line is object && ReturnHeaderPattern.IsMatch(line))
+ {
+ break;
+ }
+
+ line = mdFileReader.ReadLine();
+ }
+ }
+
+ // Search for return value documentation
+ while (line is object)
+ {
+ Match m = ReturnHeaderPattern.Match(line);
+ if (m.Success)
+ {
+ while ((line = mdFileReader.ReadLine()) is object)
+ {
+ if (line.StartsWith('#'))
+ {
+ break;
+ }
+
+ docBuilder.AppendLine(line);
+ }
+
+ docs.ReturnValue = docBuilder.ToString().Trim();
+ docBuilder.Clear();
+ break;
+ }
+ else
+ {
+ line = mdFileReader.ReadLine();
+ }
+ }
+
+ return (properName, docs, enumsByParameter, enumsByField);
+ }
+ catch (Exception ex)
+ {
+ throw new ApplicationException($"Failed parsing \"{filePath}\".", ex);
+ }
+ }
+
+ private class YamlSectionReader : TextReader
+ {
+ private readonly StreamReader fileReader;
+ private bool firstLineRead;
+ private bool lastLineRead;
+
+ internal YamlSectionReader(StreamReader fileReader)
+ {
+ this.fileReader = fileReader;
+ }
+
+ public override string? ReadLine()
+ {
+ if (this.lastLineRead)
+ {
+ return null;
+ }
+
+ if (!this.firstLineRead)
+ {
+ Expect("---", this.fileReader.ReadLine());
+ this.firstLineRead = true;
+ }
+
+ string? line = this.fileReader.ReadLine();
+ if (line == "---")
+ {
+ this.lastLineRead = true;
+ return null;
+ }
+
+ return line;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ this.fileReader.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+ }
+ }
+}
diff --git a/apidocs/ScrapeDocs/ScrapeDocs.csproj b/apidocs/ScrapeDocs/ScrapeDocs.csproj
new file mode 100644
index 00000000..b899a058
--- /dev/null
+++ b/apidocs/ScrapeDocs/ScrapeDocs.csproj
@@ -0,0 +1,18 @@
+
+
+
+ Exe
+ net5.0
+ false
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apidocs/global.json b/apidocs/global.json
new file mode 100644
index 00000000..5ecd5ee6
--- /dev/null
+++ b/apidocs/global.json
@@ -0,0 +1,5 @@
+{
+ "sdk": {
+ "version": "5.0.301"
+ }
+}
diff --git a/apidocs/stylecop.json b/apidocs/stylecop.json
new file mode 100644
index 00000000..2453d57c
--- /dev/null
+++ b/apidocs/stylecop.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
+ "settings": {
+ "documentationRules": {
+ "companyName": "Microsoft Corporation",
+ "copyrightText": "Copyright (c) {companyName}. All rights reserved.\nLicensed under the {licenseName} license. See {licenseFile} file in the project root for full license information.",
+ "variables": {
+ "licenseName": "MIT",
+ "licenseFile": "LICENSE"
+ },
+ "fileNamingConvention": "metadata",
+ "xmlHeader": false
+ }
+ }
+}
diff --git a/apidocs/version.json b/apidocs/version.json
new file mode 100644
index 00000000..356015b5
--- /dev/null
+++ b/apidocs/version.json
@@ -0,0 +1,9 @@
+{
+ "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
+ "version": "0.1-alpha",
+ "versionHeightOffset": 0, // manually +1 each time the ext/sdk-api submodule tree is updated
+ "pathFilters": [
+ ".",
+ "../ext/sdk-api" // doesn't work yet: https://github.com/dotnet/Nerdbank.GitVersioning/issues/625
+ ]
+}
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index a267b792..7253fb80 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -2,7 +2,11 @@ trigger:
branches:
include:
- master
-pr: none
+pr:
+- master
+
+variables:
+ BuildConfiguration: Release
jobs:
- job: scrape_x64
@@ -22,7 +26,7 @@ jobs:
inputs:
packageType: 'sdk'
version: '3.x'
-
+
- task: PowerShell@2
displayName: Set build version
inputs:
@@ -41,7 +45,7 @@ jobs:
arguments: '-arch x64'
errorActionPreference: 'continue'
pwsh: true
-
+
- task: PowerShell@2
displayName: Scrape constants
inputs:
@@ -73,7 +77,7 @@ jobs:
inputs:
packageType: 'sdk'
version: '3.x'
-
+
- task: PowerShell@2
displayName: GenerateMetadataSource.ps1 - x86
inputs:
@@ -81,7 +85,7 @@ jobs:
arguments: '-arch x86'
errorActionPreference: 'continue'
pwsh: true
-
+
- publish: 'generation\emitter\generated\x86'
displayName: Publish x86 emitter assets
artifact: 'emitter_generated_x86'
@@ -102,7 +106,7 @@ jobs:
inputs:
packageType: 'sdk'
version: '3.x'
-
+
- task: PowerShell@2
displayName: GenerateMetadataSource.ps1 - arm64
inputs:
@@ -110,7 +114,7 @@ jobs:
arguments: '-arch arm64'
errorActionPreference: 'continue'
pwsh: true
-
+
- publish: 'generation\emitter\generated\arm64'
displayName: Publish arm64 emitter assets
artifact: 'emitter_generated_arm64'
@@ -139,8 +143,8 @@ jobs:
- task: UseDotNet@2
displayName: Install DotNet 2.1.x for signing tasks
inputs:
- packageType: 'sdk'
- version: '2.1.x'
+ packageType: runtime
+ version: 2.1.x
- task: DownloadPipelineArtifact@2
displayName: Download x64 scraper obj assets
@@ -172,7 +176,7 @@ jobs:
filePath: 'scripts\BuildMetadataBin.ps1'
arguments: '-arch crossarch -SkipConstants'
pwsh: true
-
+
- publish: 'bin'
artifact: 'bin'
@@ -223,8 +227,8 @@ jobs:
SessionTimeout: '60'
MaxConcurrency: '50'
MaxRetryAttempts: '2'
- condition: eq(variables['SignFiles'], 'true')
-
+ condition: and(succeeded(), eq(variables['SignFiles'], 'true'))
+
# There's a problem on microsoft.visualstudio.com that requires the guid instead of NuGetCommand@2
- task: EsrpCodeSigning@1
@@ -268,8 +272,8 @@ jobs:
SessionTimeout: '60'
MaxConcurrency: '50'
MaxRetryAttempts: '2'
- condition: eq(variables['SignFiles'], 'true')
-
+ condition: and(succeeded(), eq(variables['SignFiles'], 'true'))
+
- task: PowerShell@2
displayName: Pack metadata package
inputs:
@@ -317,14 +321,14 @@ jobs:
SessionTimeout: '60'
MaxConcurrency: '50'
MaxRetryAttempts: '2'
- condition: eq(variables['SignFiles'], 'true')
-
+ condition: and(succeeded(), eq(variables['SignFiles'], 'true'))
+
- task: PublishPipelineArtifact@1
displayName: 'Publish NuGet packages to pipeline artifacts'
inputs:
targetPath: '$(OutputPackagesDir)'
artifact: NuGetPackages
-
+
# There's a problem on microsoft.visualstudio.com that requires the guid instead of NuGetCommand@2
# Don't publish if we're using pre-generated source
- task: 333b11bd-d341-40d9-afcf-b32d5ce6f23b@2
@@ -334,3 +338,70 @@ jobs:
packagesToPush: '$(OutputPackagesDir)/**/*.nupkg;!$(OutputPackagesDir)/**/*.symbols.nupkg'
publishVstsFeed: 'c1408dcb-1833-4ae4-9af5-1a891a12cc3c'
allowPackageConflicts: true
+
+- job: build_docs
+ displayName: Build API docs
+ pool:
+ vmImage: ubuntu-20.04
+ steps:
+ - checkout: self
+ clean: true
+ submodules: recursive
+ - task: UseDotNet@2
+ displayName: ⚙ Install .NET SDK
+ inputs:
+ packageType: sdk
+ useGlobalJson: true
+ workingDirectory: apidocs
+
+ # ESRP Authenticode sign package DLLs
+ - task: UseDotNet@2
+ displayName: ⚙ Install .NET Core 2.1.x
+ inputs:
+ packageType: runtime
+ version: 2.1.x
+
+ - script: dotnet pack -c $(BuildConfiguration)
+ displayName: 📦 dotnet pack
+ workingDirectory: apidocs
+ - task: EsrpCodeSigning@1
+ displayName: ✒ NuGet sign
+ inputs:
+ ConnectedServiceName: Undocked RegFree Signing Connection
+ FolderPath: $(System.DefaultWorkingDirectory)/bin/Packages/$(BuildConfiguration)/NuGet
+ Pattern: '*.nupkg'
+ signConfigType: inlineSignParams
+ inlineOperation: |
+ [
+ {
+ "KeyCode" : "CP-401405",
+ "OperationCode" : "NuGetSign",
+ "Parameters" : {},
+ "ToolName" : "sign",
+ "ToolVersion" : "1.0"
+ },
+ {
+ "KeyCode" : "CP-401405",
+ "OperationCode" : "NuGetVerify",
+ "Parameters" : {},
+ "ToolName" : "sign",
+ "ToolVersion" : "1.0"
+ }
+ ]
+ SessionTimeout: 60
+ MaxConcurrency: 50
+ MaxRetryAttempts: 5
+ condition: and(succeeded(), eq(variables['SignFiles'], 'true'))
+ - publish: bin/Packages/$(BuildConfiguration)/NuGet
+ artifact: ApiDocsNuGetPackages
+ displayName: 📢 Publish package
+ # There's a problem on microsoft.visualstudio.com that requires the guid instead of NuGetCommand@2
+ # Don't publish if we're using pre-generated source
+ - task: 333b11bd-d341-40d9-afcf-b32d5ce6f23b@2
+ displayName: 📤 NuGet push
+ inputs:
+ command: push
+ packagesToPush: $(System.DefaultWorkingDirectory)/bin/Packages/$(BuildConfiguration)/NuGet/*.nupkg
+ publishVstsFeed: c1408dcb-1833-4ae4-9af5-1a891a12cc3c
+ allowPackageConflicts: true
+ condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'))
diff --git a/ext/sdk-api b/ext/sdk-api
index bd3c10cc..c2511992 160000
--- a/ext/sdk-api
+++ b/ext/sdk-api
@@ -1 +1 @@
-Subproject commit bd3c10cc5c89ef1209ff51c2e85217e3f1936b29
+Subproject commit c251199235b283ada4e0c5afe352077f7453a680
diff --git a/strongname.snk b/strongname.snk
new file mode 100644
index 00000000..1a45d560
Binary files /dev/null and b/strongname.snk differ
|