Files
AtomUI/scripts/verification/PackageBuildContract.ps1

99 lines
6.5 KiB
PowerShell

# Compare the package with the SDK's evaluated pack contract, in addition to absolute role/TFM rules.
function Get-AtomUICanonicalPackageNode {
param([System.Xml.XmlNode]$Node)
if ($null -eq $Node) { return "" }
$attributes = @($Node.Attributes | Where-Object { $_.NamespaceURI -ne 'http://www.w3.org/2000/xmlns/' } |
Sort-Object LocalName -CaseSensitive | ForEach-Object { "$($_.LocalName)=$($_.Value)" })
$children = @($Node.ChildNodes | Where-Object NodeType -eq ([System.Xml.XmlNodeType]::Element) |
ForEach-Object { Get-AtomUICanonicalPackageNode $_ } | Sort-Object -CaseSensitive)
return ConvertTo-Json -Compress -Depth 20 -InputObject ([ordered]@{ name = $Node.LocalName; attributes = $attributes; children = $children })
}
function Assert-AtomUIPackageBuildContract {
param($Archive, [xml]$Nuspec, $Package, [string]$BuildType, [string]$Version)
$requestedPackage = $Package
. (Join-Path $PSScriptRoot '../NuGetPackageProjects.ps1')
$Package = $requestedPackage
$definition = @($AtomUIReleasePackages | Where-Object PackageId -ceq $Package.PackageId)
if ($definition.Count -ne 1 -or $definition[0].PackageRole -cne $Package.PackageRole) {
throw "Package '$($Package.PackageId)' does not match the authoritative package-role manifest."
}
$project = $definition[0].ProjectPath
$projectName = [IO.Path]::GetFileNameWithoutExtension($project)
$expectedPath = Join-Path $repositoryRoot ".artifacts/$projectName/obj/$BuildType/$($Package.PackageId).$Version.nuspec"
$basePath = [IO.Path]::GetDirectoryName($project)
if (Test-Path -LiteralPath $expectedPath -PathType Leaf) {
[xml]$expected = Get-Content -LiteralPath $expectedPath -Raw
}
else {
# Explicit nuspec projects (the aggregate language package) have no generated nuspec.
# Ask MSBuild for its evaluated source/base instead of keeping a second package list here.
$evaluation = & dotnet msbuild $project "-p:Configuration=$BuildType" '-getProperty:NuspecFile,NuspecBasePath'
if ($LASTEXITCODE -ne 0) { throw "Cannot read evaluated pack contract for $project." }
$properties = ($evaluation -join "`n" | ConvertFrom-Json).Properties
if ([string]::IsNullOrWhiteSpace($properties.NuspecFile)) {
throw "Missing generated pack contract '$expectedPath'. Build/pack the project before verification."
}
$expectedPath = if ([IO.Path]::IsPathFullyQualified($properties.NuspecFile)) { $properties.NuspecFile } else { Join-Path ([IO.Path]::GetDirectoryName($project)) $properties.NuspecFile }
$basePath = if ([IO.Path]::IsPathFullyQualified($properties.NuspecBasePath)) { $properties.NuspecBasePath } else { Join-Path ([IO.Path]::GetDirectoryName($project)) $properties.NuspecBasePath }
$expectedPath = [IO.Path]::GetFullPath($expectedPath)
$basePath = [IO.Path]::GetFullPath($basePath)
[xml]$expected = (Get-Content -LiteralPath $expectedPath -Raw).Replace('$version$', $Version)
}
if ([string]$expected.package.metadata.id -cne $Package.PackageId -or [string]$expected.package.metadata.version -cne $Version) {
throw "Evaluated pack contract '$expectedPath' has an unexpected package identity/version."
}
foreach ($section in @('dependencies', 'frameworkReferences', 'contentFiles')) {
$xpath = "/*[local-name()='package']/*[local-name()='metadata']/*[local-name()='$section']"
if ((Get-AtomUICanonicalPackageNode $expected.SelectSingleNode($xpath)) -cne
(Get-AtomUICanonicalPackageNode $Nuspec.SelectSingleNode($xpath))) {
throw "$($Package.PackageId) packaged $section differs from its evaluated build contract."
}
}
$expectedFiles = @{}
foreach ($file in $expected.SelectNodes("/*[local-name()='package']/*[local-name()='files']/*[local-name()='file']")) {
$source = [string]$file.src
if (-not [IO.Path]::IsPathFullyQualified($source)) { $source = Join-Path $basePath $source }
$target = ([string]$file.target).Replace('\', '/').TrimStart('/')
if ($target.Length -eq 0 -or $target.EndsWith('/')) { $target += [IO.Path]::GetFileName($source) }
if ($source.Contains('*') -or $target.Contains('*')) {
throw "Unexpanded pack file rule in '$expectedPath'; supply the evaluated concrete pack contract."
}
$expectedFiles[$target] = $source
if ($null -eq $Archive.GetEntry($target)) { throw "$($Package.PackageId) is missing required pack asset '$target'." }
}
foreach ($entry in $Archive.Entries) {
if ($entry.FullName -match '^(lib|ref|runtimes|analyzers|tools|build|buildTransitive|content|contentFiles)/' -and
-not $expectedFiles.ContainsKey($entry.FullName)) {
throw "$($Package.PackageId) contains an undeclared consumer asset '$($entry.FullName)'."
}
if ($entry.FullName -match '^build(?:Transitive)?/.*\.(props|targets)$') {
$reader = [IO.StreamReader]::new($entry.Open())
try { [xml]$buildAsset = $reader.ReadToEnd() } finally { $reader.Dispose() }
foreach ($import in $buildAsset.SelectNodes("//*[local-name()='Import']")) {
$projectPath = [string]$import.Project
# Static package-local imports form a deliverable closure even when the branch is
# not used by the current framework. Dynamic SDK/user imports are outside this rule.
if ($projectPath -match '^\$\(MSBuildThisFileDirectory\)([^$%@?*]+)$') {
$relative = $Matches[1].Replace('\', '/')
$resolved = [Uri]::new([Uri]::new("https://atomui.invalid/$($entry.FullName)"), $relative)
$target = [Uri]::UnescapeDataString($resolved.AbsolutePath.TrimStart('/'))
if ($resolved.Host -ne 'atomui.invalid' -or $null -eq $Archive.GetEntry($target)) {
throw "$($Package.PackageId) build asset '$($entry.FullName)' imports missing package asset '$target'."
}
}
}
}
}
foreach ($target in $expectedFiles.Keys) {
$stream = $Archive.GetEntry($target).Open()
$sha = [Security.Cryptography.SHA256]::Create()
try { $actualHash = [Convert]::ToHexString($sha.ComputeHash($stream)) }
finally { $stream.Dispose(); $sha.Dispose() }
if ($actualHash -cne (Get-FileHash -LiteralPath $expectedFiles[$target] -Algorithm SHA256).Hash) {
throw "$($Package.PackageId) contains stale or mismatched bytes for '$target'."
}
}
}