From 1991c7b8ddccc6286db84a6d0147dcfb59cd7efb Mon Sep 17 00:00:00 2001
From: Jackson Schuster <36744439+jtschuster@users.noreply.github.com>
Date: Mon, 18 Mar 2024 15:33:56 -0700
Subject: [PATCH] Add initial SDK test project for unified build
---
src/SourceBuild/content/build.proj | 1 +
.../eng/unifiedBuildvalidation.targets | 28 +-
.../.gitignore | 484 ++++++++++++++++++
.../BaselineHelper.cs | 135 +++++
.../DotNetHelper.cs | 341 ++++++++++++
.../DotNetTemplate.cs | 21 +
.../Exclusions.cs | 46 ++
.../ExecuteHelper.cs | 132 +++++
...Microsoft.DotNet.UnifiedBuild.Tests.csproj | 15 -
.../SdkContentTests.cs | 253 +++++++++
.../SdkTests.cs | 20 +
.../SkippableFactAttribute.cs | 53 ++
.../TestBase.cs | 25 +
.../Utilities.cs | 222 ++++++++
.../assets/NativeDlls-win-any.txt | 49 ++
.../assets/NativeDlls-win-x64.txt | 14 +
.../assets/NativeDlls.txt | 16 +
.../SdkAssemblyVersionDiffExclusions.txt | 3 -
...s.diff => MsftToSbSdkFiles-linux-x64.diff} | 0
.../baselines/MsftToSbSdkFiles-win-x64.diff | 0
20 files changed, 1837 insertions(+), 21 deletions(-)
create mode 100644 src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/.gitignore
create mode 100644 src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/BaselineHelper.cs
create mode 100644 src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/DotNetHelper.cs
create mode 100644 src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/DotNetTemplate.cs
create mode 100644 src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/Exclusions.cs
create mode 100644 src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/ExecuteHelper.cs
create mode 100644 src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/SdkContentTests.cs
create mode 100644 src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/SdkTests.cs
create mode 100644 src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/SkippableFactAttribute.cs
create mode 100644 src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/TestBase.cs
create mode 100644 src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/Utilities.cs
create mode 100644 src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/assets/NativeDlls-win-any.txt
create mode 100644 src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/assets/NativeDlls-win-x64.txt
create mode 100644 src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/assets/NativeDlls.txt
rename src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/assets/baselines/{MsftToSbSdkFiles.diff => MsftToSbSdkFiles-linux-x64.diff} (100%)
create mode 100644 src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/assets/baselines/MsftToSbSdkFiles-win-x64.diff
diff --git a/src/SourceBuild/content/build.proj b/src/SourceBuild/content/build.proj
index 66ed6262b..fb64e7ddd 100644
--- a/src/SourceBuild/content/build.proj
+++ b/src/SourceBuild/content/build.proj
@@ -27,6 +27,7 @@
+
$([MSBuild]::NormalizeDirectory('$(RepoRoot)', 'test', 'Microsoft.DotNet.UnifiedBuild.Tests'))
-
+
-
+ %(SdkTarballItem.Identity)
normal
+
+
+
+
+
+
+
+
+
+
+
+
+ <_BuiltSdkArchivePath>@(_BuiltSdkArchivePath)
+
+
+
diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/.gitignore b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/.gitignore
new file mode 100644
index 000000000..dd315077a
--- /dev/null
+++ b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/.gitignore
@@ -0,0 +1,484 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from `dotnet new gitignore`
+
+# dotenv files
+.env
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# Tye
+.tye/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.tlog
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio 6 auto-generated project file (contains which files were open etc.)
+*.vbp
+
+# Visual Studio 6 workspace and project file (working project files containing files to include in project)
+*.dsw
+*.dsp
+
+# Visual Studio 6 technical files
+*.ncb
+*.aps
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# Visual Studio History (VSHistory) files
+.vshistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# Local History for Visual Studio Code
+.history/
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# JetBrains Rider
+*.sln.iml
+.idea/
+
+##
+## Visual studio for Mac
+##
+
+
+# globs
+Makefile.in
+*.userprefs
+*.usertasks
+config.make
+config.status
+aclocal.m4
+install-sh
+autom4te.cache/
+*.tar.gz
+tarballs/
+test-results/
+
+# Mac bundle stuff
+*.dmg
+*.app
+
+# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
+# Windows thumbnail cache files
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# Vim temporary swap files
+*.swp
diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/BaselineHelper.cs b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/BaselineHelper.cs
new file mode 100644
index 000000000..4beb6245a
--- /dev/null
+++ b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/BaselineHelper.cs
@@ -0,0 +1,135 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using Microsoft.Extensions.FileSystemGlobbing;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Microsoft.DotNet.SourceBuild.SmokeTests
+{
+ internal class BaselineHelper
+ {
+ private const string SemanticVersionPlaceholder = "x.y.z";
+ private const string SemanticVersionPlaceholderMatchingPattern = "*.*.*"; // wildcard pattern used to match on the version represented by the placeholder
+ private const string NonSemanticVersionPlaceholder = "x.y";
+ private const string NonSemanticVersionPlaceholderMatchingPattern = "*.*"; // wildcard pattern used to match on the version represented by the placeholder
+
+ public static void CompareEntries(string baselineFileName, IOrderedEnumerable actualEntries)
+ {
+ IEnumerable baseline = File.ReadAllLines(GetBaselineFilePath(baselineFileName));
+ string[] missingEntries = actualEntries.Except(baseline).ToArray();
+ string[] extraEntries = baseline.Except(actualEntries).ToArray();
+
+ string? message = null;
+ if (missingEntries.Length > 0)
+ {
+ message = $"Missing entries in '{baselineFileName}' baseline: {Environment.NewLine}{string.Join(Environment.NewLine, missingEntries)}{Environment.NewLine}{Environment.NewLine}";
+ }
+
+ if (extraEntries.Length > 0)
+ {
+ message += $"Extra entries in '{baselineFileName}' baseline: {Environment.NewLine}{string.Join(Environment.NewLine, extraEntries)}{Environment.NewLine}{Environment.NewLine}";
+ }
+
+ Assert.Null(message);
+ }
+
+ public static void CompareBaselineContents(string baselineFileName, string actualContents, ITestOutputHelper outputHelper, bool warnOnDiffs = false, string baselineSubDir = "")
+ {
+ string actualFilePath = Path.Combine(TestBase.LogsDirectory, $"Updated{baselineFileName}");
+ File.WriteAllText(actualFilePath, actualContents);
+
+ CompareFiles(GetBaselineFilePath(baselineFileName, baselineSubDir), actualFilePath, outputHelper, warnOnDiffs);
+ }
+
+ public static void CompareFiles(string expectedFilePath, string actualFilePath, ITestOutputHelper outputHelper, bool warnOnDiffs = false)
+ {
+ string baselineFileText = File.ReadAllText(expectedFilePath).Trim();
+ string actualFileText = File.ReadAllText(actualFilePath).Trim();
+
+ string? message = null;
+
+ if (baselineFileText != actualFileText)
+ {
+ // Retrieve a diff in order to provide a UX which calls out the diffs.
+ string diff = DiffFiles(expectedFilePath, actualFilePath, outputHelper);
+ string prefix = warnOnDiffs ? "##vso[task.logissue type=warning;]" : string.Empty;
+ message = $"{Environment.NewLine}{prefix}Expected file '{expectedFilePath}' does not match actual file '{actualFilePath}`. {Environment.NewLine}"
+ + $"{diff}{Environment.NewLine}";
+
+ if (warnOnDiffs)
+ {
+ outputHelper.WriteLine(message);
+ outputHelper.WriteLine("##vso[task.complete result=SucceededWithIssues;]");
+ }
+ }
+
+ if (!warnOnDiffs)
+ {
+ Assert.Null(message);
+ }
+ }
+
+ public static string DiffFiles(string file1Path, string file2Path, ITestOutputHelper outputHelper)
+ {
+ (Process Process, string StdOut, string StdErr) diffResult =
+ ExecuteHelper.ExecuteProcess("git", $"diff --no-index {file1Path} {file2Path}", outputHelper);
+
+ return diffResult.StdOut;
+ }
+
+ public static string GetAssetsDirectory() => Path.Combine(Directory.GetCurrentDirectory(), "assets");
+
+ public static string GetBaselineFilePath(string baselineFileName, string baselineSubDir = "") =>
+ Path.Combine(GetAssetsDirectory(), "baselines", baselineSubDir, baselineFileName);
+
+ public static string RemoveRids(string diff, bool isPortable = false) =>
+ isPortable ? diff.Replace(Config.PortableRid, "portable-rid") : diff.Replace(Config.TargetRid, "banana-rid");
+
+ public static string RemoveVersions(string source)
+ {
+ // Remove version numbers for examples like "roslyn4.1", "net8.0", and "netstandard2.1".
+ string pathSeparator = $"[{Regex.Escape(@"\")}|{Regex.Escape(@"/")}]";
+ string result = Regex.Replace(source, $@"{pathSeparator}(net|roslyn)[1-9]+\.[0-9]+{pathSeparator}", match =>
+ {
+ string wordPart = match.Groups[1].Value;
+ return $"{Path.DirectorySeparatorChar}{wordPart}{NonSemanticVersionPlaceholder}{Path.DirectorySeparatorChar}";
+ });
+
+ // Remove semantic versions
+ // Regex source: https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
+ // The regex from https://semver.org has been modified to account for the following:
+ // - The version should be preceded by a path separator, '.', '-', or '/'
+ // - The version should match a release identifier that begins with '.' or '-'
+ // - The version may have one or more release identifiers that begin with '.' or '-'
+ // - The version should end before a path separator, '.', '-', or '/'
+ Regex semanticVersionRegex = new(
+ @"(?<=[./\\-_])(0|[1-9]\d*)\.(0|[1-9]\d*)(\.(0|[1-9]\d*))+"
+ + @"(((?:[-.]((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)))+"
+ + @"(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?"
+ + @"(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?(?=[/\\.-_])");
+ return semanticVersionRegex.Replace(result, SemanticVersionPlaceholder);
+ }
+
+ ///
+ /// This returns a that can be used to match on a path whose versions have been removed via
+ /// .
+ ///
+ public static Matcher GetFileMatcherFromPath(string path)
+ {
+ path = path
+ .Replace(SemanticVersionPlaceholder, SemanticVersionPlaceholderMatchingPattern)
+ .Replace(NonSemanticVersionPlaceholder, NonSemanticVersionPlaceholderMatchingPattern);
+ Matcher matcher = new();
+ matcher.AddInclude(path);
+ return matcher;
+ }
+ }
+}
diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/DotNetHelper.cs b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/DotNetHelper.cs
new file mode 100644
index 000000000..c662eaf76
--- /dev/null
+++ b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/DotNetHelper.cs
@@ -0,0 +1,341 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Sockets;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Microsoft.DotNet.SourceBuild.SmokeTests;
+
+internal class DotNetHelper
+{
+ private static readonly object s_lockObj = new();
+
+ public static string DotNetPath { get; } = Path.Combine(Config.DotNetDirectory, "dotnet");
+ public static string PackagesDirectory { get; } = Path.Combine(Directory.GetCurrentDirectory(), "packages");
+ public static string ProjectsDirectory { get; } = Path.Combine(Directory.GetCurrentDirectory(), $"projects-{DateTime.Now:yyyyMMddHHmmssffff}");
+
+ private ITestOutputHelper OutputHelper { get; }
+ public bool IsMonoRuntime { get; }
+
+ public DotNetHelper(ITestOutputHelper outputHelper)
+ {
+ OutputHelper = outputHelper;
+
+ lock (s_lockObj)
+ {
+ if (!Directory.Exists(Config.DotNetDirectory))
+ {
+ if (!File.Exists(Config.SdkTarballPath))
+ {
+ throw new InvalidOperationException($"Tarball path '{Config.SdkTarballPath}' specified in {Config.SdkTarballPath} does not exist.");
+ }
+
+ Directory.CreateDirectory(Config.DotNetDirectory);
+ Utilities.ExtractTarball(Config.SdkTarballPath, Config.DotNetDirectory, outputHelper);
+ }
+ IsMonoRuntime = DetermineIsMonoRuntime(Config.DotNetDirectory);
+
+ // if (!Directory.Exists(ProjectsDirectory))
+ // {
+ // Directory.CreateDirectory(ProjectsDirectory);
+ // InitNugetConfig();
+ // }
+
+ // if (!Directory.Exists(PackagesDirectory))
+ // {
+ // Directory.CreateDirectory(PackagesDirectory);
+ // }
+ }
+ }
+
+ private static void InitNugetConfig()
+ {
+ bool useLocalPackages = !string.IsNullOrEmpty(Config.PrereqsPath);
+ string nugetConfigPrefix = useLocalPackages ? "local" : "online";
+ string nugetConfigPath = Path.Combine(ProjectsDirectory, "NuGet.Config");
+ File.Copy(
+ Path.Combine(BaselineHelper.GetAssetsDirectory(), $"{nugetConfigPrefix}.NuGet.Config"),
+ nugetConfigPath);
+
+ if (useLocalPackages)
+ {
+ // When using local packages this feed is always required. It contains packages that are
+ // not produced by source-build but are required by the various project templates.
+ if (!Directory.Exists(Config.PrereqsPath))
+ {
+ throw new InvalidOperationException(
+ $"Prereqs path '{Config.PrereqsPath}' specified in {Config.PrereqsPathEnv} does not exist.");
+ }
+
+ string nugetConfig = File.ReadAllText(nugetConfigPath);
+ nugetConfig = nugetConfig.Replace("SMOKE_TEST_PACKAGE_FEED", Config.PrereqsPath);
+
+ // This package feed is optional. You can use an additional feed of source-built packages to run the
+ // smoke-tests as offline as possible.
+ if (Config.CustomPackagesPath != null)
+ {
+ if (!Directory.Exists(Config.CustomPackagesPath))
+ {
+ throw new ArgumentException($"Specified --with-packages {Config.CustomPackagesPath} does not exist.");
+ }
+ nugetConfig = nugetConfig.Replace("CUSTOM_PACKAGE_FEED", Config.CustomPackagesPath);
+ }
+ else
+ {
+ nugetConfig = string.Join(Environment.NewLine, nugetConfig.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Where(s => !s.Contains("CUSTOM_PACKAGE_FEED")).ToArray());
+ }
+ File.WriteAllText(nugetConfigPath, nugetConfig);
+ }
+ }
+
+ public void ExecuteCmd(string args, string? workingDirectory = null, Action? processConfigCallback = null,
+ int? expectedExitCode = 0, int millisecondTimeout = -1)
+ {
+ (Process Process, string StdOut, string StdErr) executeResult = ExecuteHelper.ExecuteProcess(
+ DotNetPath,
+ args,
+ OutputHelper,
+ configureCallback: (process) => configureProcess(process, workingDirectory),
+ millisecondTimeout: millisecondTimeout);
+
+ if (expectedExitCode != null) {
+ ExecuteHelper.ValidateExitCode(executeResult, (int) expectedExitCode);
+ }
+
+ void configureProcess(Process process, string? workingDirectory)
+ {
+ ConfigureProcess(process, workingDirectory);
+
+ processConfigCallback?.Invoke(process);
+ }
+ }
+
+ public static void ConfigureProcess(Process process, string? workingDirectory)
+ {
+ if (workingDirectory != null)
+ {
+ process.StartInfo.WorkingDirectory = workingDirectory;
+ }
+
+ process.StartInfo.EnvironmentVariables["DOTNET_CLI_TELEMETRY_OPTOUT"] = "1";
+ process.StartInfo.EnvironmentVariables["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "1";
+ process.StartInfo.EnvironmentVariables["DOTNET_ROOT"] = Config.DotNetDirectory;
+ process.StartInfo.EnvironmentVariables["NUGET_PACKAGES"] = PackagesDirectory;
+ process.StartInfo.EnvironmentVariables["PATH"] = $"{Config.DotNetDirectory}:{Environment.GetEnvironmentVariable("PATH")}";
+ }
+
+ public void ExecuteBuild(string projectName) =>
+ ExecuteCmd($"build {GetBinLogOption(projectName, "build")}", GetProjectDirectory(projectName));
+
+ ///
+ /// Create a new .NET project and return the path to the created project folder.
+ ///
+ public string ExecuteNew(string projectType, string name, string? language = null, string? customArgs = null)
+ {
+ string projectDirectory = GetProjectDirectory(name);
+ string options = $"--name {name} --output {projectDirectory}";
+ if (language != null)
+ {
+ options += $" --language \"{language}\"";
+ }
+ if (string.IsNullOrEmpty(customArgs))
+ {
+ options += $" {customArgs}";
+ }
+
+ ExecuteCmd($"new {projectType} {options}");
+
+ return projectDirectory;
+ }
+
+ public void ExecutePublish(string projectName, DotNetTemplate template, bool? selfContained = null, string? rid = null, bool trimmed = false, bool readyToRun = false)
+ {
+ string options = string.Empty;
+ string binlogDifferentiator = string.Empty;
+
+ if (selfContained.HasValue)
+ {
+ options += $"--self-contained {selfContained.Value.ToString().ToLowerInvariant()}";
+ if (selfContained.Value)
+ {
+ binlogDifferentiator += "self-contained";
+ if (!string.IsNullOrEmpty(rid))
+ {
+ options += $" -r {rid}";
+ binlogDifferentiator += $"-{rid}";
+ }
+ if (trimmed)
+ {
+ options += " /p:PublishTrimmed=true";
+ binlogDifferentiator += "-trimmed";
+ }
+ if (readyToRun)
+ {
+ options += " /p:PublishReadyToRun=true";
+ binlogDifferentiator += "-R2R";
+ }
+ }
+ }
+
+ string projDir = GetProjectDirectory(projectName);
+ string publishDir = Path.Combine(projDir, "bin", "publish");
+
+ ExecuteCmd(
+ $"publish {options} {GetBinLogOption(projectName, "publish", binlogDifferentiator)} -o {publishDir}",
+ projDir);
+
+ if (template == DotNetTemplate.Console)
+ {
+ ExecuteCmd($"{projectName}.dll", publishDir, expectedExitCode: 0);
+ }
+ else if (template == DotNetTemplate.ClassLib || template == DotNetTemplate.BlazorWasm)
+ {
+ // Can't run the published output of classlib (no entrypoint) or WASM (needs a server)
+ }
+ // Assume it is a web-based template
+ else
+ {
+ ExecuteWebDll(projectName, publishDir, template);
+ }
+ }
+
+ public void ExecuteRun(string projectName) =>
+ ExecuteCmd($"run {GetBinLogOption(projectName, "run")}", GetProjectDirectory(projectName));
+
+ public void ExecuteRunWeb(string projectName, DotNetTemplate template)
+ {
+ // 'dotnet run' exit code differs between CoreCLR and Mono (https://github.com/dotnet/sdk/issues/30095).
+ int expectedExitCode = IsMonoRuntime ? 143 : 0;
+
+ ExecuteWeb(
+ projectName,
+ $"run --no-launch-profile {GetBinLogOption(projectName, "run")}",
+ GetProjectDirectory(projectName),
+ template,
+ expectedExitCode);
+ }
+
+ public void ExecuteWebDll(string projectName, string workingDirectory, DotNetTemplate template) =>
+ ExecuteWeb(projectName, $"{projectName}.dll", workingDirectory, template, expectedExitCode: 0);
+
+ public void ExecuteTest(string projectName) =>
+ ExecuteCmd($"test {GetBinLogOption(projectName, "test")}", GetProjectDirectory(projectName));
+
+ private void ExecuteWeb(string projectName, string args, string workingDirectory, DotNetTemplate template, int expectedExitCode)
+ {
+ WebAppValidator validator = new(OutputHelper, template);
+ ExecuteCmd(
+ args,
+ workingDirectory,
+ processConfigCallback: validator.Validate,
+ expectedExitCode: expectedExitCode,
+ millisecondTimeout: 30000);
+ Assert.True(validator.IsValidated);
+ if (validator.ValidationException is not null)
+ {
+ throw validator.ValidationException;
+ }
+ }
+
+ private static string GetBinLogOption(string projectName, string command, string? differentiator = null)
+ {
+ string fileName = $"{projectName}-{command}";
+ if (!string.IsNullOrEmpty(differentiator))
+ {
+ fileName += $"-{differentiator}";
+ }
+
+ return $"/bl:{Path.Combine(TestBase.LogsDirectory, $"{fileName}.binlog")}";
+ }
+
+ private static bool DetermineIsMonoRuntime(string dotnetRoot)
+ {
+ string sharedFrameworkRoot = Path.Combine(dotnetRoot, "shared", "Microsoft.NETCore.App");
+ if (!Directory.Exists(sharedFrameworkRoot))
+ {
+ return false;
+ }
+
+ string? version = Directory.GetDirectories(sharedFrameworkRoot).FirstOrDefault();
+ if (version is null)
+ {
+ return false;
+ }
+
+ string sharedFramework = Path.Combine(sharedFrameworkRoot, version);
+
+ // Check the presence of one of the mono header files.
+ return File.Exists(Path.Combine(sharedFramework, "mono-gc.h"));
+ }
+
+ private static string GetProjectDirectory(string projectName) => Path.Combine(ProjectsDirectory, projectName);
+
+ public static bool ShouldPublishComplex() =>
+ !string.Equals(Config.TargetArchitecture,"ppc64le") && !string.Equals(Config.TargetArchitecture,"s390x");
+
+ private class WebAppValidator
+ {
+ private readonly ITestOutputHelper _outputHelper;
+ private readonly DotNetTemplate _template;
+
+ public WebAppValidator(ITestOutputHelper outputHelper, DotNetTemplate template)
+ {
+ _outputHelper = outputHelper;
+ _template = template;
+ }
+
+ public bool IsValidated { get; set; }
+ public Exception? ValidationException { get; set; }
+
+ private static int GetAvailablePort()
+ {
+ TcpListener listener = new(IPAddress.Loopback, 0);
+ listener.Start();
+ int port = ((IPEndPoint)listener.LocalEndpoint).Port;
+ listener.Stop();
+ return port;
+ }
+
+ public void Validate(Process process)
+ {
+ int port = GetAvailablePort();
+ process.StartInfo.EnvironmentVariables.Add("ASPNETCORE_HTTP_PORTS", port.ToString());
+ process.OutputDataReceived += new DataReceivedEventHandler((sender, e) =>
+ {
+ try
+ {
+ if (e.Data?.Contains("Application started. Press Ctrl+C to shut down.") ?? false)
+ {
+ _outputHelper.WriteLine("Detected app has started. Sending web request to validate...");
+
+ using HttpClient httpClient = new();
+ string url = $"http://localhost:{port}";
+ if (_template == DotNetTemplate.WebApi)
+ {
+ url += "/WeatherForecast";
+ }
+
+ using HttpResponseMessage resultMsg = httpClient.GetAsync(new Uri(url)).Result;
+ _outputHelper.WriteLine($"Status code returned: {resultMsg.StatusCode}");
+ resultMsg.EnsureSuccessStatusCode();
+ IsValidated = true;
+
+ ExecuteHelper.ExecuteProcessValidateExitCode("kill", $"-s TERM {process.Id}", _outputHelper);
+ }
+ }
+ catch (Exception ex)
+ {
+ ValidationException = ex;
+ }
+ });
+ }
+ }
+}
diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/DotNetTemplate.cs b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/DotNetTemplate.cs
new file mode 100644
index 000000000..0a30c7215
--- /dev/null
+++ b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/DotNetTemplate.cs
@@ -0,0 +1,21 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace Microsoft.DotNet.SourceBuild.SmokeTests;
+
+public enum DotNetTemplate
+{
+ Console,
+ ClassLib,
+ XUnit,
+ NUnit,
+ MSTest,
+ Web,
+ Mvc,
+ Razor,
+ BlazorWasm,
+ WebApi,
+ WebApp,
+ Worker,
+}
diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/Exclusions.cs b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/Exclusions.cs
new file mode 100644
index 000000000..63c58dafd
--- /dev/null
+++ b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/Exclusions.cs
@@ -0,0 +1,46 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.IO.Enumeration;
+using System.Linq;
+using Microsoft.DotNet.SourceBuild.SmokeTests;
+
+public class Exclusions
+{
+ string _rid = Config.TargetRid;
+
+ string[] GetRidSpecificExclusionFileNames(string path)
+ {
+ string filename = Path.GetFileNameWithoutExtension(path);
+ string extension = Path.GetExtension(path);
+ Debug.Assert(path == $"{filename}{extension}", $"{path} != {filename}{extension}");
+ string[] parts = _rid.Split('-');
+ string[] fileNames = new string[parts.Length+1];
+ fileNames[0] = $"{filename}{extension}";
+ for (int i = 1; i < parts.Length; i++)
+ {
+ fileNames[i] = $"{filename}-{string.Join('-', parts[..i])}-any{extension}";
+ }
+ fileNames[parts.Length] = $"{filename}-{_rid}{extension}";
+ return fileNames;
+ }
+
+ public List GetFileExclusions(string? prefix = null) => GetRidSpecificExclusionFileNames("SdkFileDiffExclusions.txt").SelectMany(f => Utilities.TryParseExclusionsFile(f, prefix)).ToList();
+ public List GetAssemblyVersionExclusions(string? prefix = null) => GetRidSpecificExclusionFileNames("SdkAssemblyVersionDiffExclusions.txt").SelectMany(f => Utilities.TryParseExclusionsFile(f, prefix)).ToList();
+ public List GetNativeDllExclusions(string? prefix = null) => GetRidSpecificExclusionFileNames("NativeDlls.txt").SelectMany(f => Utilities.TryParseExclusionsFile(f, prefix)).ToList();
+ public string GetBaselineFileDiffFileName() => GetRidSpecificExclusionFileNames("MsftToSbSdkFiles.diff").Last();
+
+
+ string NormalizePath(string path)
+ {
+ return path.Replace('\\', '/');
+ }
+
+ bool IsFileExcluded(string file, string? prefix = null)
+ => GetFileExclusions(prefix).Any(exclusion => FileSystemName.MatchesSimpleExpression(exclusion, NormalizePath(file)));
+}
diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/ExecuteHelper.cs b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/ExecuteHelper.cs
new file mode 100644
index 000000000..129d96c24
--- /dev/null
+++ b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/ExecuteHelper.cs
@@ -0,0 +1,132 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Diagnostics;
+using System.Linq;
+using System.Text;
+using Xunit.Abstractions;
+
+namespace Microsoft.DotNet.SourceBuild.SmokeTests;
+
+internal static class ExecuteHelper
+{
+ public static (Process Process, string StdOut, string StdErr) ExecuteProcess(
+ string fileName,
+ string args,
+ ITestOutputHelper outputHelper,
+ bool logOutput = false,
+ bool excludeInfo = false,
+ Action? configureCallback = null,
+ int millisecondTimeout = -1)
+ {
+ if (!excludeInfo)
+ {
+ outputHelper.WriteLine($"Executing: {fileName} {args}");
+ }
+
+ Process process = new()
+ {
+ EnableRaisingEvents = true,
+ StartInfo =
+ {
+ FileName = fileName,
+ Arguments = args,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ }
+ };
+
+ // The `dotnet test` execution context sets a number of dotnet related ENVs that cause issues when executing
+ // dotnet commands. Clear these to avoid side effects.
+ foreach (string key in process.StartInfo.Environment.Keys.Where(key => key != "HOME").ToList())
+ {
+ process.StartInfo.Environment.Remove(key);
+ }
+
+ configureCallback?.Invoke(process);
+
+ StringBuilder stdOutput = new();
+ process.OutputDataReceived += new DataReceivedEventHandler(
+ (sender, e) =>
+ {
+ lock (stdOutput)
+ {
+ stdOutput.AppendLine(e.Data);
+ }
+ });
+
+ StringBuilder stdError = new();
+ process.ErrorDataReceived += new DataReceivedEventHandler(
+ (sender, e) =>
+ {
+ lock (stdError)
+ {
+ stdError.AppendLine(e.Data);
+ }
+ });
+
+ process.Start();
+
+ process.BeginOutputReadLine();
+ process.BeginErrorReadLine();
+ process.WaitForExit(millisecondTimeout);
+
+ if (!process.HasExited)
+ {
+ outputHelper.WriteLine($"Process did not exit. Killing {fileName} {args} after waiting {millisecondTimeout} milliseconds.");
+ process.Kill(true);
+ process.WaitForExit();
+ }
+
+ string output;
+ string error;
+
+ lock (stdOutput)
+ {
+ output = stdOutput.ToString().Trim();
+ }
+
+ lock (stdError)
+ {
+ error = stdError.ToString().Trim();
+ }
+
+ if (logOutput)
+ {
+ if (!string.IsNullOrWhiteSpace(output))
+ {
+ outputHelper.WriteLine(output);
+ }
+
+ if (string.IsNullOrWhiteSpace(error))
+ {
+ outputHelper.WriteLine(error);
+ }
+ }
+
+ return (process, output, error);
+ }
+
+ public static string ExecuteProcessValidateExitCode(string fileName, string args, ITestOutputHelper outputHelper)
+ {
+ (Process Process, string StdOut, string StdErr) result = ExecuteHelper.ExecuteProcess(fileName, args, outputHelper);
+ ValidateExitCode(result);
+
+ return result.StdOut;
+ }
+
+ public static void ValidateExitCode((Process Process, string StdOut, string StdErr) result, int expectedExitCode = 0)
+ {
+ if (result.Process.ExitCode != expectedExitCode)
+ {
+ ProcessStartInfo startInfo = result.Process.StartInfo;
+ string msg = $"Failed to execute {startInfo.FileName} {startInfo.Arguments}" +
+ $"{Environment.NewLine}Exit code: {result.Process.ExitCode}" +
+ $"{Environment.NewLine}{result.StdOut}" +
+ $"{Environment.NewLine}{result.StdErr}";
+ throw new InvalidOperationException(msg);
+ }
+ }
+}
diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/Microsoft.DotNet.UnifiedBuild.Tests.csproj b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/Microsoft.DotNet.UnifiedBuild.Tests.csproj
index a7830912f..289665581 100644
--- a/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/Microsoft.DotNet.UnifiedBuild.Tests.csproj
+++ b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/Microsoft.DotNet.UnifiedBuild.Tests.csproj
@@ -15,21 +15,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/SdkContentTests.cs b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/SdkContentTests.cs
new file mode 100644
index 000000000..4ae0d304d
--- /dev/null
+++ b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/SdkContentTests.cs
@@ -0,0 +1,253 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Data;
+using System.IO;
+using System.IO.Enumeration;
+using System.Linq;
+using System.Reflection;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Microsoft.Extensions.FileSystemGlobbing;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Microsoft.DotNet.SourceBuild.SmokeTests;
+
+[Trait("Category", "SdkContent")]
+public class SdkContentTests : SdkTests
+{
+ private const string MsftSdkType = "msft";
+ private const string SourceBuildSdkType = "sb";
+
+ public SdkContentTests(ITestOutputHelper outputHelper) : base(outputHelper) { }
+
+ ///
+ /// Verifies the file layout of the source built sdk tarball to the Microsoft build.
+ /// The differences are captured in baselines/MsftToSbSdkDiff.txt.
+ /// Version numbers that appear in paths are compared but are stripped from the baseline.
+ /// This makes the baseline durable between releases. This does mean however, entries
+ /// in the baseline may appear identical if the diff is version specific.
+ ///
+ [SkippableFact(new[] { Config.MsftSdkTarballPathEnv, Config.SdkTarballPathEnv }, skipOnNullOrWhiteSpaceEnv: true)]
+ public void CompareMsftToSbFileList()
+ {
+ const string msftFileListingFileName = "msftSdkFiles.txt";
+ const string sbFileListingFileName = "sbSdkFiles.txt";
+ WriteTarballFileList(Config.MsftSdkTarballPath, msftFileListingFileName, isPortable: true, MsftSdkType);
+ WriteTarballFileList(Config.SdkTarballPath, sbFileListingFileName, isPortable: true, SourceBuildSdkType);
+
+ string diff = BaselineHelper.DiffFiles(msftFileListingFileName, sbFileListingFileName, OutputHelper);
+ diff = RemoveDiffMarkers(diff);
+ BaselineHelper.CompareBaselineContents(new Exclusions().GetBaselineFileDiffFileName(), diff, OutputHelper, Config.WarnOnSdkContentDiffs);
+ }
+
+ [SkippableFact(new[] { Config.MsftSdkTarballPathEnv, Config.SdkTarballPathEnv }, skipOnNullOrWhiteSpaceEnv: true)]
+ public void CompareMsftToSbAssemblyVersions()
+ {
+ Assert.NotNull(Config.MsftSdkTarballPath);
+ Assert.NotNull(Config.SdkTarballPath);
+
+ DirectoryInfo tempDir = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()));
+ try
+ {
+ DirectoryInfo sbSdkDir = Directory.CreateDirectory(Path.Combine(tempDir.FullName, SourceBuildSdkType));
+ Utilities.ExtractTarball(Config.SdkTarballPath, sbSdkDir.FullName, OutputHelper);
+
+ DirectoryInfo msftSdkDir = Directory.CreateDirectory(Path.Combine(tempDir.FullName, MsftSdkType));
+ Utilities.ExtractTarball(Config.MsftSdkTarballPath, msftSdkDir.FullName, OutputHelper);
+
+ var t1 = Task.Run(() => GetSdkAssemblyVersions(sbSdkDir.FullName));
+ var t2 = Task.Run(() => GetSdkAssemblyVersions(msftSdkDir.FullName));
+ Task.WaitAll(t1, t2);
+ Dictionary sbSdkAssemblyVersions = t1.Result;
+ Dictionary msftSdkAssemblyVersions = t2.Result;
+
+ RemoveExcludedAssemblyVersionPaths(sbSdkAssemblyVersions, msftSdkAssemblyVersions);
+
+ const string SbVersionsFileName = "sb_assemblyversions.txt";
+ WriteAssemblyVersionsToFile(sbSdkAssemblyVersions, SbVersionsFileName);
+
+ const string MsftVersionsFileName = "msft_assemblyversions.txt";
+ WriteAssemblyVersionsToFile(msftSdkAssemblyVersions, MsftVersionsFileName);
+
+ string diff = BaselineHelper.DiffFiles(MsftVersionsFileName, SbVersionsFileName, OutputHelper);
+ diff = RemoveDiffMarkers(diff);
+ BaselineHelper.CompareBaselineContents("MsftToSbSdkAssemblyVersions.diff", diff, OutputHelper, Config.WarnOnSdkContentDiffs);
+ }
+ finally
+ {
+ tempDir.Delete(recursive: true);
+ }
+ }
+
+ private static void RemoveExcludedAssemblyVersionPaths(Dictionary sbSdkAssemblyVersions, Dictionary msftSdkAssemblyVersions)
+ {
+ IEnumerable assemblyVersionDiffFilters = GetSdkAssemblyVersionDiffExclusionFilters()
+ .Select(filter => filter.TrimStart("./".ToCharArray()));
+
+ // Remove any excluded files as long as SB SDK's file has the same or greater assembly version compared to the corresponding
+ // file in the MSFT SDK. If the version is less, the file will show up in the results as this is not a scenario
+ // that is valid for shipping.
+ string[] sbSdkFileArray = sbSdkAssemblyVersions.Keys.ToArray();
+ for (int i = sbSdkFileArray.Length - 1; i >= 0; i--)
+ {
+ string assemblyPath = sbSdkFileArray[i];
+ Version? sbVersion = sbSdkAssemblyVersions[assemblyPath];
+ Version? msftVersion = msftSdkAssemblyVersions[assemblyPath];
+
+ if (sbVersion is not null &&
+ msftVersion is not null &&
+ sbVersion >= msftVersion &&
+ Utilities.IsFileExcluded(assemblyPath, assemblyVersionDiffFilters))
+ {
+ sbSdkAssemblyVersions.Remove(assemblyPath);
+ msftSdkAssemblyVersions.Remove(assemblyPath);
+ }
+ }
+ }
+
+ private static void WriteAssemblyVersionsToFile(Dictionary assemblyVersions, string outputPath)
+ {
+ string[] lines = assemblyVersions
+ .Select(kvp => $"{kvp.Key} - {kvp.Value}")
+ .Order()
+ .ToArray();
+ File.WriteAllLines(outputPath, lines);
+ }
+
+ // It's known that assembly versions can be different between builds in their revision field. Disregard that difference
+ // by excluding that field in the output.
+ private static Version? GetVersion(AssemblyName assemblyName)
+ {
+ if (assemblyName.Version is not null)
+ {
+ return new Version(assemblyName.Version.ToString(3));
+ }
+
+ return null;
+ }
+
+ private string FindMatchingFilePath(string rootDir, Matcher matcher, string representativeFile)
+ {
+ foreach (string file in Directory.EnumerateFiles(rootDir, "*", SearchOption.AllDirectories))
+ {
+ if (matcher.Match(rootDir, file).HasMatches)
+ {
+ return file;
+ }
+ }
+
+ Assert.Fail($"Unable to find matching file for '{representativeFile}' in '{rootDir}'.");
+ return string.Empty;
+ }
+
+ private Dictionary GetSdkAssemblyVersions(string sbSdkPath)
+ {
+ Exclusions ex = new Exclusions();
+ IEnumerable exclusionFilters = GetSdkDiffExclusionFilters(SourceBuildSdkType)
+ .Concat(GetKnownNativeFiles())
+ .Select(filter => filter.TrimStart("./".ToCharArray()));
+ List knownNativeFiles = Utilities.ParseExclusionsFile("NativeDlls-win-any.txt").ToList();
+ ConcurrentDictionary sbSdkAssemblyVersions = new();
+ List tasks = new List();
+ foreach (string dir in Directory.EnumerateDirectories(sbSdkPath, "*", SearchOption.AllDirectories).Append(sbSdkPath))
+ {
+ var t = Task.Run(() =>
+ {
+ foreach (string file in Directory.EnumerateFiles(dir, "*", SearchOption.TopDirectoryOnly))
+ {
+ string fileExt = Path.GetExtension(file);
+ if (fileExt.Equals(".dll", StringComparison.OrdinalIgnoreCase) ||
+ fileExt.Equals(".exe", StringComparison.OrdinalIgnoreCase))
+ {
+ string relativePath = Path.GetRelativePath(sbSdkPath, file);
+ string normalizedPath = BaselineHelper.RemoveVersions(relativePath);
+ if (!Utilities.IsFileExcluded(normalizedPath, exclusionFilters))
+ {
+ try
+ {
+ AssemblyName assemblyName = AssemblyName.GetAssemblyName(file);
+ Assert.True(sbSdkAssemblyVersions.TryAdd(normalizedPath, GetVersion(assemblyName)));
+ }
+ catch (BadImageFormatException)
+ {
+ Console.WriteLine($"BadImageFormatException: {file}");
+ }
+ }
+ }
+ }
+ });
+ tasks.Add(t);
+ }
+ //foreach (string file in Directory.EnumerateFiles(sbSdkPath, "*", SearchOption.AllDirectories))
+ //{
+ // string fileExt = Path.GetExtension(file);
+ // if (fileExt.Equals(".dll", StringComparison.OrdinalIgnoreCase) ||
+ // fileExt.Equals(".exe", StringComparison.OrdinalIgnoreCase))
+ // {
+ // string relativePath = Path.GetRelativePath(sbSdkPath, file);
+ // string normalizedPath = BaselineHelper.RemoveVersions(relativePath);
+ // if (!Utilities.IsFileExcluded(normalizedPath, exclusionFilters))
+ // {
+ // var t = Task.Run(() =>
+ // {
+ // try
+ // {
+ // AssemblyName assemblyName = AssemblyName.GetAssemblyName(file);
+ // sbSdkAssemblyVersions.Add(normalizedPath, GetVersion(assemblyName));
+ // }
+ // catch (BadImageFormatException)
+ // {
+ // Console.WriteLine($"BadImageFormatException: {file}");
+ // }
+ // });
+ // tasks.Add(t);
+ // }
+ // }
+ //}
+ Task.WaitAll(tasks.ToArray());
+ return sbSdkAssemblyVersions.ToDictionary();
+ }
+
+ private void WriteTarballFileList(string? tarballPath, string outputFileName, bool isPortable, string sdkType)
+ {
+ if (!File.Exists(tarballPath))
+ {
+ throw new InvalidOperationException($"Tarball path '{tarballPath}' does not exist.");
+ }
+
+ string fileListing = Utilities.GetTarballContentNames(tarballPath).Aggregate((a, b) => $"{a}{Environment.NewLine}{b}");
+ fileListing = BaselineHelper.RemoveRids(fileListing, isPortable);
+ fileListing = BaselineHelper.RemoveVersions(fileListing);
+ IEnumerable files = fileListing.Split(Environment.NewLine).OrderBy(path => path);
+ files = RemoveExclusions(files, GetSdkDiffExclusionFilters(sdkType));
+
+ File.WriteAllLines(outputFileName, files);
+ }
+
+ private static IEnumerable RemoveExclusions(IEnumerable files, IEnumerable exclusions) =>
+ files.Where(item => !Utilities.IsFileExcluded(item, exclusions));
+
+ private static IEnumerable GetSdkDiffExclusionFilters(string sdkType) =>
+ Utilities.ParseExclusionsFile("SdkFileDiffExclusions.txt", sdkType);
+
+ private static IEnumerable GetSdkAssemblyVersionDiffExclusionFilters() =>
+ Utilities.ParseExclusionsFile("SdkAssemblyVersionDiffExclusions.txt");
+
+ private static IEnumerable GetKnownNativeFiles() =>
+ Utilities.ParseExclusionsFile("NativeDlls-win-any.txt");
+
+ private static string RemoveDiffMarkers(string source)
+ {
+ Regex indexRegex = new("^index .*", RegexOptions.Multiline);
+ string result = indexRegex.Replace(source, "index ------------");
+
+ Regex diffSegmentRegex = new("^@@ .* @@", RegexOptions.Multiline);
+ return diffSegmentRegex.Replace(result, "@@ ------------ @@");
+ }
+}
diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/SdkTests.cs b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/SdkTests.cs
new file mode 100644
index 000000000..ba3381360
--- /dev/null
+++ b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/SdkTests.cs
@@ -0,0 +1,20 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Xunit.Abstractions;
+
+namespace Microsoft.DotNet.SourceBuild.SmokeTests;
+
+///
+/// Shared base class for all SDK-based smoke tests.
+///
+public abstract class SdkTests : TestBase
+{
+ internal DotNetHelper DotNetHelper { get; }
+
+ protected SdkTests(ITestOutputHelper outputHelper) : base(outputHelper)
+ {
+ DotNetHelper = new DotNetHelper(outputHelper);
+ }
+}
diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/SkippableFactAttribute.cs b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/SkippableFactAttribute.cs
new file mode 100644
index 000000000..c1314e19e
--- /dev/null
+++ b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/SkippableFactAttribute.cs
@@ -0,0 +1,53 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Linq;
+using Xunit;
+
+namespace Microsoft.DotNet.SourceBuild.SmokeTests;
+
+///
+/// A Fact that will be skipped based on the specified environment variable's value.
+///
+internal class SkippableFactAttribute : FactAttribute
+{
+ public SkippableFactAttribute(string envName, bool skipOnNullOrWhiteSpaceEnv = false, bool skipOnTrueEnv = false, bool skipOnFalseEnv = false, string[] skipArchitectures = null) =>
+ EvaluateSkips(skipOnNullOrWhiteSpaceEnv, skipOnTrueEnv, skipOnFalseEnv, skipArchitectures, (skip) => Skip = skip, envName);
+
+ public SkippableFactAttribute(string[] envNames, bool skipOnNullOrWhiteSpaceEnv = false, bool skipOnTrueEnv = false, bool skipOnFalseEnv = false, string[] skipArchitectures = null) =>
+ EvaluateSkips(skipOnNullOrWhiteSpaceEnv, skipOnTrueEnv, skipOnFalseEnv, skipArchitectures, (skip) => Skip = skip, envNames);
+
+ public static void EvaluateSkips(bool skipOnNullOrWhiteSpaceEnv, bool skipOnTrueEnv, bool skipOnFalseEnv, string[] skipArchitectures, Action setSkip, params string[] envNames)
+ {
+ foreach (string envName in envNames)
+ {
+ string? envValue = Environment.GetEnvironmentVariable(envName);
+
+ if (skipOnNullOrWhiteSpaceEnv && string.IsNullOrWhiteSpace(envValue))
+ {
+ setSkip($"Skipping because `{envName}` is null or whitespace");
+ break;
+ }
+ else if (skipOnTrueEnv && bool.TryParse(envValue, out bool boolValue) && boolValue)
+ {
+ setSkip($"Skipping because `{envName}` is set to True");
+ break;
+ }
+ else if (skipOnFalseEnv && (!bool.TryParse(envValue, out boolValue) || !boolValue))
+ {
+ setSkip($"Skipping because `{envName}` is set to False or an invalid value");
+ break;
+ }
+ }
+
+ if (skipArchitectures != null) {
+ string? arch = Config.TargetArchitecture;
+ if (skipArchitectures.Contains(arch))
+ {
+ setSkip($"Skipping because arch is `{arch}`");
+ }
+ }
+ }
+}
diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/TestBase.cs b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/TestBase.cs
new file mode 100644
index 000000000..963f07109
--- /dev/null
+++ b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/TestBase.cs
@@ -0,0 +1,25 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.IO;
+using Xunit.Abstractions;
+
+namespace Microsoft.DotNet.SourceBuild.SmokeTests;
+
+public abstract class TestBase
+{
+ public static string LogsDirectory { get; } = Path.Combine(Directory.GetCurrentDirectory(), "logs");
+
+ public ITestOutputHelper OutputHelper { get; }
+
+ public TestBase(ITestOutputHelper outputHelper)
+ {
+ OutputHelper = outputHelper;
+
+ if (!Directory.Exists(LogsDirectory))
+ {
+ Directory.CreateDirectory(LogsDirectory);
+ }
+ }
+}
diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/Utilities.cs b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/Utilities.cs
new file mode 100644
index 000000000..54939d835
--- /dev/null
+++ b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/Utilities.cs
@@ -0,0 +1,222 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using Microsoft.Extensions.FileSystemGlobbing;
+using System;
+using System.Collections.Generic;
+using System.Formats.Tar;
+using System.IO;
+using System.IO.Compression;
+using System.IO.Enumeration;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Microsoft.DotNet.SourceBuild.SmokeTests;
+
+public static class Utilities
+{
+ ///
+ /// Returns whether the given file path is excluded by the given exclusions using glob file matching.
+ ///
+ public static bool IsFileExcluded(string filePath, IEnumerable exclusions) =>
+ GetMatchingFileExclusions(filePath.Replace('\\', '/'), exclusions, exclusion => exclusion).Any();
+
+ public static IEnumerable GetMatchingFileExclusions(string filePath, IEnumerable exclusions, Func getExclusionExpression) =>
+ exclusions.Where(exclusion => FileSystemName.MatchesSimpleExpression(getExclusionExpression(exclusion), filePath));
+
+ ///
+ /// Parses a common file format in the test suite for listing file exclusions.
+ ///
+ /// Name of the exclusions file.
+ /// When specified, filters the exclusions to those that begin with the prefix value.
+ public static IEnumerable ParseExclusionsFile(string exclusionsFileName, string? prefix = null)
+ {
+ string exclusionsFilePath = Path.Combine(BaselineHelper.GetAssetsDirectory(), exclusionsFileName);
+ int prefixSkip = prefix?.Length + 1 ?? 0;
+ return File.ReadAllLines(exclusionsFilePath)
+ // process only specific exclusions if a prefix is provided
+ .Where(line => prefix is null || line.StartsWith(prefix + ","))
+ .Select(line =>
+ {
+ // Ignore comments
+ var index = line.IndexOf('#');
+ return index >= 0 ? line[prefixSkip..index].TrimEnd() : line[prefixSkip..];
+ })
+ .Where(line => !string.IsNullOrEmpty(line))
+ .ToList();
+ }
+
+ public static IEnumerable TryParseExclusionsFile(string exclusionsFileName, string? prefix = null)
+ {
+ string exclusionsFilePath = Path.Combine(BaselineHelper.GetAssetsDirectory(), exclusionsFileName);
+ int prefixSkip = prefix?.Length + 1 ?? 0;
+ if (!File.Exists(exclusionsFilePath))
+ {
+ return [];
+ }
+ return File.ReadAllLines(exclusionsFilePath)
+ // process only specific exclusions if a prefix is provided
+ .Where(line => prefix is null || line.StartsWith(prefix + ","))
+ .Select(line =>
+ {
+ // Ignore comments
+ var index = line.IndexOf('#');
+ return index >= 0 ? line[prefixSkip..index].TrimEnd() : line[prefixSkip..];
+ })
+ .Where(line => !string.IsNullOrEmpty(line))
+ .ToList();
+ }
+
+ public static void ExtractTarball(string tarballPath, string outputDir, ITestOutputHelper outputHelper)
+ {
+ // TarFile doesn't properly handle hard links (https://github.com/dotnet/runtime/pull/85378#discussion_r1221817490),
+ // use 'tar' instead.
+ if (tarballPath.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase) || tarballPath.EndsWith(".tgz", StringComparison.OrdinalIgnoreCase))
+ {
+ ExecuteHelper.ExecuteProcessValidateExitCode("tar", $"xzf {tarballPath} -C {outputDir}", outputHelper);
+ }
+ else if (tarballPath.EndsWith(".zip"))
+ {
+ ZipFile.ExtractToDirectory(tarballPath, outputDir);
+ }
+ else
+ {
+ throw new InvalidOperationException($"Unsupported tarball format: {tarballPath}");
+ }
+ }
+
+ public static void ExtractTarball(string tarballPath, string outputDir, string targetFilePath)
+ {
+ Matcher matcher = new();
+ matcher.AddInclude(targetFilePath);
+
+ using FileStream fileStream = File.OpenRead(tarballPath);
+ using GZipStream decompressorStream = new(fileStream, CompressionMode.Decompress);
+ using TarReader reader = new(decompressorStream);
+
+ TarEntry entry;
+ while ((entry = reader.GetNextEntry()) is not null)
+ {
+ if (matcher.Match(entry.Name).HasMatches)
+ {
+ string outputPath = Path.Join(outputDir, entry.Name);
+ Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
+
+ using FileStream outputFileStream = File.Create(outputPath);
+ entry.DataStream.CopyTo(outputFileStream);
+ break;
+ }
+ }
+ }
+
+ public static IEnumerable GetTarballContentNames(string tarballPath)
+ {
+ if (tarballPath.EndsWith(".zip"))
+ {
+ using ZipArchive zip = ZipFile.OpenRead(tarballPath);
+ foreach (ZipArchiveEntry entry in zip.Entries)
+ {
+ yield return entry.FullName;
+ }
+ yield break;
+ }
+ else if (tarballPath.EndsWith(".tar.gz") || tarballPath.EndsWith(".tgz"))
+ {
+ using FileStream fileStream = File.OpenRead(tarballPath);
+ using GZipStream decompressorStream = new(fileStream, CompressionMode.Decompress);
+ using TarReader reader = new(decompressorStream);
+
+ TarEntry entry;
+ while ((entry = reader.GetNextEntry()) is not null)
+ {
+ yield return entry.Name;
+ }
+ }
+ }
+
+ public static void ExtractNupkg(string package, string outputDir)
+ {
+ Directory.CreateDirectory(outputDir);
+
+ using ZipArchive zip = ZipFile.OpenRead(package);
+ foreach (ZipArchiveEntry entry in zip.Entries)
+ {
+ string outputPath = Path.Combine(outputDir, entry.FullName);
+ Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
+ entry.ExtractToFile(outputPath);
+ }
+ }
+
+ public static async Task RetryAsync(Func executor, ITestOutputHelper outputHelper)
+ {
+ await Utilities.RetryAsync(
+ async () =>
+ {
+ try
+ {
+ await executor();
+ return null;
+ }
+ catch (Exception e)
+ {
+ return e;
+ }
+ },
+ outputHelper);
+ }
+
+ private static async Task RetryAsync(Func> executor, ITestOutputHelper outputHelper)
+ {
+ const int maxRetries = 5;
+ const int waitFactor = 5;
+
+ int retryCount = 0;
+
+ Exception? exception = await executor();
+ while (exception != null)
+ {
+ retryCount++;
+ if (retryCount >= maxRetries)
+ {
+ throw new InvalidOperationException($"Failed after {retryCount} retries.", exception);
+ }
+
+ int waitTime = Convert.ToInt32(Math.Pow(waitFactor, retryCount - 1));
+ if (outputHelper != null)
+ {
+ outputHelper.WriteLine($"Retry {retryCount}/{maxRetries}, retrying in {waitTime} seconds...");
+ }
+
+ Thread.Sleep(TimeSpan.FromSeconds(waitTime));
+ exception = await executor();
+ }
+ }
+
+ public static void LogWarningMessage(this ITestOutputHelper outputHelper, string message)
+ {
+ string prefix = "##vso[task.logissue type=warning;]";
+
+ outputHelper.WriteLine($"{Environment.NewLine}{prefix}{message}.{Environment.NewLine}");
+ outputHelper.WriteLine("##vso[task.complete result=SucceededWithIssues;]");
+ }
+
+ public static void ValidateNotNullOrWhiteSpace(string? variable, string variableName)
+ {
+ if (string.IsNullOrWhiteSpace(variable))
+ {
+ throw new ArgumentException($"{variableName} is null, empty, or whitespace.");
+ }
+ }
+
+ public static string GetFile(string path, string pattern)
+ {
+ string[] files = Directory.GetFiles(path, pattern, SearchOption.AllDirectories);
+ Assert.False(files.Length > 1, $"Found multiple files matching the pattern {pattern}: {Environment.NewLine}{string.Join(Environment.NewLine, files)}");
+ Assert.False(files.Length == 0, $"Did not find any files matching the pattern {pattern}");
+ return files[0];
+ }
+}
diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/assets/NativeDlls-win-any.txt b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/assets/NativeDlls-win-any.txt
new file mode 100644
index 000000000..9e8e6088e
--- /dev/null
+++ b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/assets/NativeDlls-win-any.txt
@@ -0,0 +1,49 @@
+# Contains the list of files which are .dll's or .exe's but are not managed assemblies and should not have their assembly version checked
+#
+# This list is processed using FileSystemName.MatchesSimpleExpression
+#
+# Examples
+# 'folder/*' matches 'folder/' and 'folder/abc'
+# 'folder/?*' matches 'folder/abc' but not 'folder/'
+#
+# We do not want to filter-out folder entries, therefore, we should use: '?*' and not just '*'
+
+
+./sdk/x.y.z/AppHostTemplate/apphost.exe
+./host/fxr/x.y.z/hostfxr.dll
+./shared/Microsoft.AspNetCore.App/x.y.z/aspnetcorev2_inprocess.dll
+./shared/Microsoft.WindowsDesktop.App/x.y.z/D3DCompiler_47_cor3.dll
+./shared/Microsoft.NETCore.App/x.y.z/clretwrc.dll
+./shared/Microsoft.NETCore.App/x.y.z/clrgc.dll
+./shared/Microsoft.WindowsDesktop.App/x.y.z/PenImc_cor3.dll
+./shared/Microsoft.WindowsDesktop.App/x.y.z/PresentationNative_cor3.dll
+./shared/Microsoft.NETCore.App/x.y.z/clrgcexp.dll
+./shared/Microsoft.NETCore.App/x.y.z/clrjit.dll
+./shared/Microsoft.WindowsDesktop.App/x.y.z/vcruntime140_cor3.dll
+./shared/Microsoft.NETCore.App/x.y.z/coreclr.dll
+./shared/Microsoft.NETCore.App/x.y.z/createdump.exe
+./shared/Microsoft.WindowsDesktop.App/x.y.z/wpfgfx_cor3.dll
+./shared/Microsoft.NETCore.App/x.y.z/hostpolicy.dll
+./shared/Microsoft.NETCore.App/x.y.z/Microsoft.DiaSymReader.Native.amd64.dll
+./shared/Microsoft.NETCore.App/x.y.z/mscordaccore.dll
+./shared/Microsoft.NETCore.App/x.y.z/mscordaccore_amd64_amd64_x.y.z.dll
+./shared/Microsoft.NETCore.App/x.y.z/mscordbi.dll
+./shared/Microsoft.NETCore.App/x.y.z/mscorrc.dll
+./shared/Microsoft.NETCore.App/x.y.z/msquic.dll
+./packs/Microsoft.NETCore.App.Host.win-x64/x.y.z/runtimes/win-x64/native/apphost.exe
+./packs/Microsoft.NETCore.App.Host.win-x86/x.y.z/runtimes/win-x86/native/apphost.exe
+./packs/Microsoft.NETCore.App.Host.win-arm64/x.y.z/runtimes/win-arm64/native/apphost.exe
+./packs/Microsoft.NETCore.App.Host.win-x64/x.y.z/runtimes/win-x64/native/comhost.dll
+./packs/Microsoft.NETCore.App.Host.win-x86/x.y.z/runtimes/win-x86/native/comhost.dll
+./packs/Microsoft.NETCore.App.Host.win-arm64/x.y.z/runtimes/win-arm64/native/comhost.dll
+./packs/Microsoft.NETCore.App.Host.win-x64/x.y.z/runtimes/win-x64/native/ijwhost.dll
+./packs/Microsoft.NETCore.App.Host.win-x86/x.y.z/runtimes/win-x86/native/ijwhost.dll
+./packs/Microsoft.NETCore.App.Host.win-arm64/x.y.z/runtimes/win-arm64/native/ijwhost.dll
+./packs/Microsoft.NETCore.App.Host.win-x64/x.y.z/runtimes/win-x64/native/nethost.dll
+./packs/Microsoft.NETCore.App.Host.win-x86/x.y.z/runtimes/win-x86/native/nethost.dll
+./packs/Microsoft.NETCore.App.Host.win-arm64/x.y.z/runtimes/win-arm64/native/nethost.dll
+./packs/Microsoft.NETCore.App.Host.win-x64/x.y.z/runtimes/win-x64/native/singlefilehost.exe
+./packs/Microsoft.NETCore.App.Host.win-x86/x.y.z/runtimes/win-x86/native/singlefilehost.exe
+./packs/Microsoft.NETCore.App.Host.win-arm64/x.y.z/runtimes/win-arm64/native/singlefilehost.exe
+./shared/Microsoft.NETCore.App/x.y.z/System.IO.Compression.Native.dll
+./dotnet.exe
\ No newline at end of file
diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/assets/NativeDlls-win-x64.txt b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/assets/NativeDlls-win-x64.txt
new file mode 100644
index 000000000..e93e1d37b
--- /dev/null
+++ b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/assets/NativeDlls-win-x64.txt
@@ -0,0 +1,14 @@
+# Contains the list of files whose assembly versions are to be excluded from comparison between the MSFT & SB SDK.
+# These exclusions only take effect if the assembly version of the file in the SB SDK is equal to or greater than
+# the version in the MSFT SDK. If the version is less, the file will show up in the results as this is not a scenario
+# that is valid for shipping.
+#
+# This list is processed using FileSystemName.MatchesSimpleExpression
+#
+# Examples
+# 'folder/*' matches 'folder/' and 'folder/abc'
+# 'folder/?*' matches 'folder/abc' but not 'folder/'
+#
+# We do not want to filter-out folder entries, therefore, we should use: '?*' and not just '*'
+
+./shared/Microsoft.NETCore.App/x.y.z/mscordaccore_amd64_amd64_x.y.z.dll
\ No newline at end of file
diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/assets/NativeDlls.txt b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/assets/NativeDlls.txt
new file mode 100644
index 000000000..1b5390e2a
--- /dev/null
+++ b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/assets/NativeDlls.txt
@@ -0,0 +1,16 @@
+# Contains the list of files whose assembly versions are to be excluded from comparison between the MSFT & SB SDK.
+# These exclusions only take effect if the assembly version of the file in the SB SDK is equal to or greater than
+# the version in the MSFT SDK. If the version is less, the file will show up in the results as this is not a scenario
+# that is valid for shipping.
+#
+# This list is processed using FileSystemName.MatchesSimpleExpression
+#
+# Examples
+# 'folder/*' matches 'folder/' and 'folder/abc'
+# 'folder/?*' matches 'folder/abc' but not 'folder/'
+#
+# We do not want to filter-out folder entries, therefore, we should use: '?*' and not just '*'
+
+./sdk/x.y.z/TestHostNetFramework/x64/msdia140.dll
+./sdk/x.y.z/TestHostNetFramework/x86/msdia140.dll
+./sdk/x.y.z/datacollector.dll
diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/assets/SdkAssemblyVersionDiffExclusions.txt b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/assets/SdkAssemblyVersionDiffExclusions.txt
index 1b5390e2a..e5b053016 100644
--- a/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/assets/SdkAssemblyVersionDiffExclusions.txt
+++ b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/assets/SdkAssemblyVersionDiffExclusions.txt
@@ -11,6 +11,3 @@
#
# We do not want to filter-out folder entries, therefore, we should use: '?*' and not just '*'
-./sdk/x.y.z/TestHostNetFramework/x64/msdia140.dll
-./sdk/x.y.z/TestHostNetFramework/x86/msdia140.dll
-./sdk/x.y.z/datacollector.dll
diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/assets/baselines/MsftToSbSdkFiles.diff b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/assets/baselines/MsftToSbSdkFiles-linux-x64.diff
similarity index 100%
rename from src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/assets/baselines/MsftToSbSdkFiles.diff
rename to src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/assets/baselines/MsftToSbSdkFiles-linux-x64.diff
diff --git a/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/assets/baselines/MsftToSbSdkFiles-win-x64.diff b/src/SourceBuild/content/test/Microsoft.DotNet.UnifiedBuild.Tests/assets/baselines/MsftToSbSdkFiles-win-x64.diff
new file mode 100644
index 000000000..e69de29bb