void CopyChangelogs (DirectoryPath diffRoot, string id, string version)
{
foreach (var (path, platform) in GetPlatformDirectories (diffRoot)) {
// first, make sure to create markdown files for unchanged assemblies
var xmlFiles = $"{path}/*.new.info.xml";
foreach (var file in GetFiles (xmlFiles)) {
var dll = file.GetFilenameWithoutExtension ().GetFilenameWithoutExtension ().GetFilenameWithoutExtension ();
var md = $"{path}/{dll}.diff.md";
if (!FileExists (md)) {
var n = Environment.NewLine;
var noChangesText = $"# API diff: {dll}{n}{n}## {dll}{n}{n}> No changes.{n}";
FileWriteText (md, noChangesText);
}
}
// now copy the markdown files to the changelogs
var mdFiles = $"{path}/*.*.md";
ReplaceTextInFiles (mdFiles, "
", "> ");
ReplaceTextInFiles (mdFiles, "
", Environment.NewLine);
ReplaceTextInFiles (mdFiles, "\r\r", "\r");
foreach (var file in GetFiles (mdFiles)) {
var dllName = file.GetFilenameWithoutExtension ().GetFilenameWithoutExtension ().GetFilenameWithoutExtension ();
if (file.GetFilenameWithoutExtension ().GetExtension () == ".breaking") {
// skip over breaking changes without any breaking changes
if (!FindTextInFiles (file.FullPath, "###").Any ()) {
DeleteFile (file);
continue;
}
dllName += ".breaking";
}
var changelogPath = (FilePath)$"./logs/changelogs/{id}/{version}/{dllName}.md";
EnsureDirectoryExists (changelogPath.GetDirectory ());
CopyFile (file, changelogPath);
}
}
}
Task ("docs-download-output")
.Does (async () =>
{
EnsureDirectoryExists ("./output");
CleanDirectories ("./output");
await DownloadPackageAsync ("_nugets", OUTPUT_NUGETS_PATH);
await DownloadPackageAsync ("_nugetspreview", OUTPUT_NUGETS_PATH);
foreach (var id in TRACKED_NUGETS.Keys) {
var version = GetVersion (id);
var localNugetVersion = PREVIEW_ONLY_NUGETS.Contains(id)
? $"{version}-{PREVIEW_NUGET_SUFFIX}"
: version;
var name = $"{id}.{localNugetVersion}.nupkg";
CleanDirectories ($"./output/{id}");
Unzip ($"{OUTPUT_NUGETS_PATH}/{name}", $"./output/{id}/nuget");
}
});
Task ("docs-api-diff")
.Does (async () =>
{
// working version
var baseDir = $"{OUTPUT_NUGETS_PATH}/api-diff";
CleanDirectories (baseDir);
// pretty version
var diffDir = "./output/api-diff";
EnsureDirectoryExists (diffDir);
CleanDirectories (diffDir);
Information ($"Creating comparer...");
var comparer = await CreateNuGetDiffAsync ();
comparer.SaveAssemblyApiInfo = true;
comparer.SaveAssemblyMarkdownDiff = true;
// some parts of SkiaSharp depend on other parts
comparer.SearchPaths.Add($"./output/SkiaSharp/nuget/lib/netstandard2.0");
foreach (var dir in GetDirectories($"./output/SkiaSharp.Views.Maui.Core/nuget/lib/*"))
comparer.SearchPaths.Add(dir.FullPath);
var filter = new NuGetVersions.Filter {
IncludePrerelease = NUGET_DIFF_PRERELEASE
};
foreach (var id in TRACKED_NUGETS.Keys) {
Information ($"Comparing the assemblies in '{id}'...");
var version = GetVersion (id);
var localNugetVersion = PREVIEW_ONLY_NUGETS.Contains(id)
? $"{version}-{PREVIEW_NUGET_SUFFIX}"
: version;
var latestVersion = (await NuGetVersions.GetLatestAsync (id, filter))?.ToNormalizedString ();
Debug ($"Version '{latestVersion}' is the latest version of '{id}'...");
// pre-cache so we can have better logs
if (!string.IsNullOrEmpty (latestVersion)) {
Debug ($"Caching version '{latestVersion}' of '{id}'...");
await comparer.ExtractCachedPackageAsync (id, latestVersion);
}
// generate the diff and copy to the changelogs
Debug ($"Running a diff on '{latestVersion}' vs '{localNugetVersion}' of '{id}'...");
var diffRoot = $"{baseDir}/{id}";
using (var reader = new PackageArchiveReader ($"{OUTPUT_NUGETS_PATH}/{id.ToLower ()}.{localNugetVersion}.nupkg")) {
// run the diff with just the breaking changes
comparer.MarkdownDiffFileExtension = ".breaking.md";
comparer.IgnoreNonBreakingChanges = true;
await comparer.SaveCompleteDiffToDirectoryAsync (id, latestVersion, reader, diffRoot);
// run the diff on everything
comparer.MarkdownDiffFileExtension = null;
comparer.IgnoreNonBreakingChanges = false;
await comparer.SaveCompleteDiffToDirectoryAsync (id, latestVersion, reader, diffRoot);
}
CopyChangelogs (diffRoot, id, version);
// copy pretty version
foreach (var md in GetFiles ($"{diffRoot}/*/*.md")) {
var tfm = md.GetDirectory ().GetDirectoryName();
var prettyPath = ((DirectoryPath)diffDir).CombineWithFilePath ($"{id}/{tfm}/{md.GetFilename ()}");
if (!FindTextInFiles (md.FullPath, "No changes").Any ()) {
EnsureDirectoryExists (prettyPath.GetDirectory ());
CopyFile (md, prettyPath);
}
}
Information ($"Diff complete of '{id}'.");
}
});
Task ("docs-api-diff-past")
.Does (async () =>
{
var baseDir = "./output/api-diffs-past";
CleanDirectories (baseDir);
Information ($"Creating comparer...");
var comparer = await CreateNuGetDiffAsync ();
comparer.SaveAssemblyApiInfo = true;
comparer.SaveAssemblyMarkdownDiff = true;
foreach (var id in TRACKED_NUGETS.Keys) {
Information ($"Comparing the assemblies in '{id}'...");
var allVersions = await NuGetVersions.GetAllAsync (id);
for (var idx = 0; idx < allVersions.Length; idx++) {
// get the versions for the diff
var version = allVersions [idx].ToNormalizedString ();
var previous = idx == 0 ? null : allVersions [idx - 1].ToNormalizedString ();
Information ($"Comparing version '{previous}' vs '{version}' of '{id}'...");
// pre-cache so we can have better logs
Debug ($"Caching version '{version}' of '{id}'...");
await comparer.ExtractCachedPackageAsync (id, version);
if (previous != null) {
Debug ($"Caching version '{previous}' of '{id}'...");
await comparer.ExtractCachedPackageAsync (id, previous);
}
// generate the diff and copy to the changelogs
Debug ($"Running a diff on '{previous}' vs '{version}' of '{id}'...");
var diffRoot = $"{baseDir}/{id}/{version}";
// run the diff with just the breaking changes
comparer.MarkdownDiffFileExtension = ".breaking.md";
comparer.IgnoreNonBreakingChanges = true;
await comparer.SaveCompleteDiffToDirectoryAsync (id, previous, version, diffRoot);
// run the diff on everything
comparer.MarkdownDiffFileExtension = null;
comparer.IgnoreNonBreakingChanges = false;
await comparer.SaveCompleteDiffToDirectoryAsync (id, previous, version, diffRoot);
CopyChangelogs (diffRoot, id, version);
Debug ($"Diff complete of version '{version}' of '{id}'.");
}
Information ($"Diff complete of '{id}'.");
}
// clean up after working
CleanDirectories (baseDir);
});
Task ("docs-update-frameworks")
.Does (async () =>
{
// clear the temp dir
var docsTempPath = "./output/docs/temp";
EnsureDirectoryExists (docsTempPath);
CleanDirectories (docsTempPath);
// get a comparer that will download the nugets
Information ($"Creating comparer...");
var comparer = await CreateNuGetDiffAsync ();
// generate the temp frameworks.xml
var xFrameworks = new XElement ("Frameworks");
foreach (var id in TRACKED_NUGETS.Keys) {
// skip doc generation for Uno, this is the same as UWP and it is not needed
if (id.StartsWith ("SkiaSharp.Views.Uno"))
continue;
// get the versions
Information ($"Comparing the assemblies in '{id}'...");
var allVersions = await NuGetVersions.GetAllAsync (id, new NuGetVersions.Filter {
MinimumVersion = new NuGetVersion (TRACKED_NUGETS [id])
});
// add the current dev version to the mix
var dev = new NuGetVersion (GetVersion (id));
allVersions = allVersions.Union (new [] { dev }).ToArray ();
// "merge" the patches
var merged = new Dictionary ();
foreach (var version in allVersions) {
merged [$"{version.Major}.{version.Minor}.{version.Patch}"] = version;
}
foreach (var version in merged) {
Information ($"Downloading '{id}' version '{version}'...");
// get the path to the nuget contents
var packagePath = version.Value == dev
? $"./output/{id}/nuget"
: await comparer.ExtractCachedPackageAsync (id, version.Value);
var dirs =
GetPlatformDirectories ($"{packagePath}/lib").Union(
GetPlatformDirectories ($"{packagePath}/ref"));
foreach (var (path, platform) in dirs) {
string moniker;
if (id.StartsWith ("SkiaSharp.Views.Forms"))
if (id != "SkiaSharp.Views.Forms")
continue;
else
moniker = $"skiasharp-views-forms-{version.Key}";
else if (id.StartsWith ("SkiaSharp.Views"))
moniker = $"skiasharp-views-{version.Key}";
else if (platform == null)
moniker = $"{id.ToLower ().Replace (".", "-")}-{version.Key}";
else
moniker = $"{id.ToLower ().Replace (".", "-")}-{platform}-{version.Key}";
// add the node to the frameworks.xml
if (xFrameworks.Elements ("Framework")?.Any (e => e.Attribute ("Name").Value == moniker) != true) {
xFrameworks.Add (
new XElement ("Framework",
new XAttribute ("Name", moniker),
new XAttribute ("Source", moniker)));
}
// copy the assemblies for the tool
var o = $"{docsTempPath}/{moniker}";
EnsureDirectoryExists (o);
CopyFiles ($"{path}/*.dll", o);
}
}
}
// save the frameworks.xml
var fwxml = $"{docsTempPath}/frameworks.xml";
var xdoc = new XDocument (xFrameworks);
xdoc.Save (fwxml);
// generate doc files
comparer = await CreateNuGetDiffAsync ();
var refArgs = string.Join (" ", comparer.SearchPaths.Select (r => $"--lib=\"{r}\""));
var fw = MakeAbsolute ((FilePath) fwxml);
RunProcess (MDocPath, new ProcessSettings {
Arguments = $"update --debug --delete --out=\"{DOCS_PATH}\" --lang=DocId --frameworks={fw} {refArgs}",
WorkingDirectory = docsTempPath
});
// clean up after working
CleanDirectories (docsTempPath);
});
Task ("docs-format-docs")
.Does (() =>
{
// process the generated docs
var docFiles = GetFiles ("./docs/**/*.xml");
float typeCount = 0;
float memberCount = 0;
float totalTypes = 0;
float totalMembers = 0;
foreach (var file in docFiles) {
Debug("Processing {0}...", file.FullPath);
var xdoc = XDocument.Load (file.FullPath);
// remove IComponent docs as this is just designer
if (xdoc.Root.Name == "Type") {
xdoc.Root
.Elements ("Members")
.Elements ("Member")
.Where (e => e.Attribute ("MemberName")?.Value?.StartsWith ("System.ComponentModel.IComponent.") == true)
.Remove ();
}
// remove any duplicate public keys
if (xdoc.Root.Name == "Overview") {
var multiKey = xdoc.Root
.Elements ("Assemblies")
.Elements ("Assembly")
.Where (e => e.Elements ("AssemblyPublicKey").Count () > 1);
foreach (var mass in multiKey) {
mass.Elements ("AssemblyPublicKey")
.Skip (1)
.Remove ();
}
}
// remove any assembly attributes for now: https://github.com/mono/api-doc-tools/issues/560
if (xdoc.Root.Name == "Overview") {
xdoc.Root
.Elements ("Assemblies")
.Elements ("Assembly")
.Elements ("Attributes")
.Elements ("Attribute")
.Remove ();
}
// remove any duplicate AssemblyVersions
if (xdoc.Root.Name == "Type") {
foreach (var info in xdoc.Root.Descendants ("AssemblyInfo")) {
var versions = info.Elements ("AssemblyVersion");
var newVersions = new List ();
foreach (var version in versions) {
if (newVersions.All (nv => nv.Value != version.Value)) {
newVersions.Add (version);
}
}
versions.Remove ();
info.Add (newVersions.OrderBy (e => e.Value));
}
}
// Fix the type rename from SkPath1DPathEffectStyle to SKPath1DPathEffectStyle
// this breaks linux as it is just a case change and that OS is case sensitive
if (xdoc.Root.Name == "Overview") {
xdoc.Root
.Elements ("Types")
.Elements ("Namespace")
.Elements ("Type")
.Where (e => e.Attribute ("Name")?.Value == "SkPath1DPathEffectStyle")
.Remove ();
}
// remove the duplicate SKDynamicMemoryWStream.CopyTo method with a different return type
if (xdoc.Root.Name == "Type" && xdoc.Root.Attribute ("Name")?.Value == "SKDynamicMemoryWStream") {
var copyTos = xdoc.Root
.Elements ("Members")
.Elements ("Member")
.Where (e => e.Attribute ("MemberName")?.Value == "CopyTo")
.Where (e => e.Elements ("MemberSignature").Any (s => s.Attribute ("Value")?.Value == "M:SkiaSharp.SKDynamicMemoryWStream.CopyTo(SkiaSharp.SKWStream)"));
var voidReturn = copyTos.FirstOrDefault (e => e.Element ("ReturnValue")?.Element ("ReturnType")?.Value == "System.Void");
var boolReturn = copyTos.FirstOrDefault (e => e.Element ("ReturnValue")?.Element ("ReturnType")?.Value == "System.Boolean");
if (voidReturn != null && boolReturn != null) {
boolReturn
.Element ("AssemblyInfo")
.Elements ("AssemblyVersion")
.FirstOrDefault ()
.AddBeforeSelf (voidReturn.Element ("AssemblyInfo").Elements ("AssemblyVersion"));
voidReturn.Remove ();
}
}
// remove the no-longer-obsolete document members
if (xdoc.Root.Name == "Type" && xdoc.Root.Attribute ("Name")?.Value == "SKDocument") {
xdoc.Root
.Elements ("Members")
.Elements ("Member")
.Where (e => e.Attribute ("MemberName")?.Value == "CreatePdf")
.Where (e => e.Elements ("MemberSignature").All (s => s.Attribute ("Value")?.Value != "M:SkiaSharp.SKDocument.CreatePdf(SkiaSharp.SKWStream,SkiaSharp.SKDocumentPdfMetadata,System.Single)"))
.SelectMany (e => e.Elements ("Attributes").Elements ("Attribute").Elements ("AttributeName"))
.Where (e => e.Value.Contains ("System.Obsolete"))
.Remove ();
}
// remove the no-longer-obsolete SK3dView attributes
if (xdoc.Root.Name == "Type" && xdoc.Root.Attribute ("Name")?.Value == "SK3dView") {
xdoc.Root
.Element ("Attributes")
.Elements ("Attribute")
.SelectMany (e => e.Elements ("AttributeName"))
.Where (e => e.Value.Contains ("System.Obsolete"))
.Remove ();
}
// remove empty FrameworkAlternate elements
var emptyAlts = xdoc.Root
.Descendants ()
.Where (d => d.Attribute ("FrameworkAlternate") != null && string.IsNullOrEmpty (d.Attribute ("FrameworkAlternate").Value))
.ToArray ();
foreach (var empty in emptyAlts) {
if (empty?.Parent != null) {
empty.Remove ();
}
}
// remove empty Attribute elements
xdoc.Root
.Descendants ("Attribute")
.Where (e => !e.Elements ().Any ())
.Remove ();
// special case for Android resources: don't process
if (xdoc.Root.Name == "Type") {
var nameAttr = xdoc.Root.Attribute ("FullName")?.Value;
if (nameAttr == "SkiaSharp.Views.Android.Resource" || nameAttr?.StartsWith ("SkiaSharp.Views.Android.Resource+") == true) {
DeleteFile (file);
continue;
}
}
if (xdoc.Root.Name == "Overview") {
foreach (var type in xdoc.Root.Descendants ("Type").ToArray ()) {
var nameAttr = type.Attribute ("Name")?.Value;
if (nameAttr == "Resource" || nameAttr?.StartsWith ("Resource+") == true) {
type.Remove ();
}
}
}
if (xdoc.Root.Name == "Framework") {
foreach (var type in xdoc.Root.Descendants ("Type").ToArray ()) {
var nameAttr = type.Attribute ("Name")?.Value;
if (nameAttr == "SkiaSharp.Views.Android.Resource" || nameAttr?.StartsWith ("SkiaSharp.Views.Android.Resource/") == true) {
type.Remove ();
}
}
}
// count the types without docs
var typesWithDocs = xdoc.Root
.Elements ("Docs");
totalTypes += typesWithDocs.Count ();
var currentTypeCount = typesWithDocs.Count (m => m.Value?.IndexOf ("To be added.") >= 0);
typeCount += currentTypeCount;
// count the members without docs
var membersWithDocs = xdoc.Root
.Elements ("Members")
.Elements ("Member")
.Elements ("Docs");
totalMembers += membersWithDocs.Count ();
var currentMemberCount = membersWithDocs.Count (m => m.Value?.IndexOf ("To be added.") >= 0);
memberCount += currentMemberCount;
// log if either type or member has missing docs
currentMemberCount += currentTypeCount;
if (currentMemberCount > 0) {
var fullName = xdoc.Root.Attribute ("FullName");
if (fullName != null)
Information ("Docs missing on {0} = {1}", fullName.Value, currentMemberCount);
}
// get the whitespaces right
var settings = new XmlWriterSettings {
Encoding = new UTF8Encoding (),
Indent = true,
NewLineChars = "\n",
OmitXmlDeclaration = true,
};
using (var writer = XmlWriter.Create (file.ToString (), settings)) {
xdoc.Save (writer);
writer.Flush ();
}
// empty line at the end
System.IO.File.AppendAllText (file.ToString (), "\n");
}
// log summary
Information (
"Documentation missing in {0}/{1} ({2:0.0%}) types and {3}/{4} ({5:0.0%}) members.",
typeCount, totalTypes, typeCount / totalTypes,
memberCount, totalMembers, memberCount / totalMembers);
});