eng/scripts/MavenPackaging.ps1 (167 lines of code) (raw):
# This class is the top level representation of a Maven package
# for this script. A MavenPackageDetail maps to one physical POM
# file and then contains a collection of associated artifacts
# which are useful when constructing the command to publish the
# artifacts via the Maven tooling.
class MavenPackageDetail {
[System.IO.FileInfo]$File
[string]$FullyQualifiedName
[string]$GroupID
[string]$ArtifactID
[string]$Version
[bool]$IsSnapshot
[AssociatedArtifact[]]$AssociatedArtifacts
}
# This class represents a physical file on disk which is
# grouped under a specific POM file. There will be one of
# these for the javadoc file, the sources, the main jar file
# (or aar file) and POM file. If this is a POM only package
# then there will only be one associated artifact. Additionally
# if other artifacts with different classications are present
# they will also be stored in one of these instances.
class AssociatedArtifact {
[System.IO.FileInfo]$File
[string]$Type
[string]$Classifier
}
# Given a Maven package, discover the associated artifacts and return them in an array. We
# do this by filtering all the artifacts in the directory by the artifactId and version prefix
# of the file to get the full set of artifacts, and then do processing on the filenames
# to figure out what the classifier (e.g. -sources, -javadoc and -uber) and type (extension).
function Get-AssociatedArtifacts([MavenPackageDetail]$PackageDetail) {
Write-Information "Detecting associated artifacts for $($PackageDetail.FullyQualifiedName)"
$associtedArtifactFileFilter = "$($PackageDetail.ArtifactID)-$($PackageDetail.Version)*"
Write-Information "Search filter is: $associtedArtifactFileFilter (jar, aar and pom files only)"
$associatedArtifactFiles = @(Get-ChildItem -Path $PackageDetail.File.Directory -Filter $associtedArtifactFileFilter | Where-Object { $_ -match "^*\.(jar|pom|aar|module|md)$" })
Write-Information "Found $($associatedArtifactFiles.Length) possible artifacts:"
[AssociatedArtifact[]]$associatedArtifacts = @()
foreach ($associatedArtifactFile in $associatedArtifactFiles)
{
$associatedArtifact = [AssociatedArtifact]::new()
$associatedArtifact.File = $associatedArtifactFile
Write-Information "Processsing artifact $($associatedArtifact.File.Name) of $($PackageDetail.FullyQualifiedName)"
$associatedArtifact.Type = $associatedArtifact.File.Extension.Replace(".", "")
Write-Information "Type is: $($associatedArtifact.Type)"
$artifactPrefix = "$($PackageDetail.ArtifactID)-$($PackageDetail.Version)-"
if ($associatedArtifact.File.BaseName.Contains($artifactPrefix)) {
$associatedArtifact.Classifier = $associatedArtifact.File.BaseName.Replace($artifactPrefix, "")
}
Write-Information "Classifier is: $($associatedArtifact.Classifier)"
$associatedArtifacts += $associatedArtifact
}
return $associatedArtifacts
}
# This function returns an array of object where each object represents a logical Maven package
# including all of its associated artifacts. It takes care of extracting out group ID, artifact ID,
# and version information, as well as providing metadata for each of the associated files including
# types and classifiers.
function Get-MavenPackageDetails([string]$ArtifactDirectory) {
Write-Information "Searching artifact directory for POM files."
$pomFiles = @(Get-ChildItem -Path $ArtifactDirectory -Filter *.pom -Recurse)
Write-Information "Found $($pomFiles.Length) POM files."
[MavenPackageDetail[]] $packageDetails = @()
foreach ($pomFile in $pomFiles) {
$packageDetail = [MavenPackageDetail]::new()
$packageDetail.File = $pomFile
Write-Information "Processing POM file: $pomFile"
[xml]$pomDocument = Get-Content $pomFile
$packageDetail.GroupID = $pomDocument.project.groupId
if (!$packageDetail.GroupID) { $packageDetail.GroupID = $pomDocument.project.parent.groupId }
if (!$packageDetail.GroupID) { throw "No GroupID found for $pomFile" }
Write-Information "Group ID is: $($packageDetail.GroupID)"
$packageDetail.ArtifactID = $pomDocument.project.artifactId
if (!$packageDetail.ArtifactID) { throw "No ArtifactID found for $pomFile" }
Write-Information "Artifact ID is: $($packageDetail.ArtifactID)"
$packageDetail.Version = $pomDocument.project.version
if (!$packageDetail.Version) { $packageDetail.Version = $pomDocument.project.parent.version }
if (!$packageDetail.Version) { throw "No Version found for $pomFile" }
Write-Information "Version is: $($packageDetail.Version)"
$packageDetail.IsSnapshot = $packageDetail.Version.EndsWith("-SNAPSHOT")
Write-Information "IsSnapshot is: $($packageDetail.IsSnapshot)"
$packageDetail.FullyQualifiedName = "$($packageDetail.GroupID):$($packageDetail.ArtifactID):$($packageDetail.Version)"
Write-Information "Fully-qualified name is: $($packageDetail.FullyQualifiedName)"
$associatedArtifacts = Get-AssociatedArtifacts($packageDetail)
$packageDetail.AssociatedArtifacts = $associatedArtifacts
$packageDetails += $packageDetail
}
return $packageDetails
}
# Implements filtering logic on the set of detected packages within the artifact directory
# that is specified. In theory we could do the filtering as soon as we read the artifact ID
# and group ID from the POM file, however discovering the full set of packages accessible
# under a path may be a useful diagnostic when looking into release issues.
function Get-FilteredMavenPackageDetails([string]$ArtifactDirectory, [string]$GroupIDFilter, [string]$ArtifactIDFilter) {
[MavenPackageDetail[]]$packageDetails = Get-MavenPackageDetails($ArtifactDirectory)
[MavenPackageDetail[]]$filteredPackageDetails = @()
if (($GroupIDFilter -ne "") -and ($ArtifactIDFilter -ne "")) {
$filteredPackageDetails = @($packageDetails | Where-Object { ($_.GroupID -eq $GroupIDFilter) -and ($_.ArtifactID -eq $ArtifactIDFilter) })
}
elseif (($GroupIDFilter -ne "") -or ($ArtifactIDFilter -ne "")) {
throw "You must specify both -GroupIDFilter and -ArtifactIDFilter together."
}
else {
$filteredPackageDetails = $packageDetails
}
return $filteredPackageDetails
}
# Compare the contents of SHA hash files for a local repository to the same
# files in Maven Central or Azure DevOps. Returns $false if none of the hash
# files exist in the remote, $true if all hashes exist and match the local
# hash, otherwise throws.
function Test-ReleasedPackage([string]$RepositoryUrl, [MavenPackageDetail]$PackageDetail, [string]$BearerToken) {
if ($RepositoryUrl -match "^https://pkgs.dev.azure.com/azure-sdk/\b(internal|public)\b/*") {
if (!$BearerToken) {
throw "BearerToken required for Azure DevOps package feeds"
}
$baseUrl = $RepositoryUrl
$algorithm = "sha256"
$headers = @{ Authorization="BEARER $BearerToken" }
}
elseif ($RepositoryUrl -match "^https://oss.sonatype.org/service/local/staging/deploy/maven2") {
$baseUrl = "https://repo1.maven.org/maven2"
$algorithm = "sha1"
$headers = @{ }
}
else {
throw "Repository URL must be either an Azure Artifacts feed, or a SonaType Nexus feed."
}
$packageUrl = "$baseUrl/$($PackageDetail.GroupId.Replace('.', '/'))/$($PackageDetail.ArtifactID)/$($PackageDetail.Version)"
# Count the number of remote hashes found
$remoteCount = 0
# Count the number of remote hashes that match their local hash
$matchCount = 0
foreach ($artifact in $PackageDetail.AssociatedArtifacts) {
$localFileName = $artifact.File.Name;
$remoteHashUrl = "$packageUrl/$localFileName.$algorithm"
Write-Information "Comparing local and remote hashes for $localFileName"
Write-Information " Getting remote hash"
$response = Invoke-WebRequest -Method GET -Uri $remoteHashUrl -Headers $headers -MaximumRetryCount 3 -SkipHttpErrorCheck
if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 300) {
$remoteCount++
if ($artifact.File.Extension -ieq '.jar') {
# Because authenticode signing isn't determinsitic, we can't compare the hash of 2 separately signed jars
Write-Information " Remote hash of jar file esists."
$matchCount++
}
else {
$remoteHash = $response.Content
Write-Information " Getting local hash"
$localPath = $artifact.File.FullName
$localHash = Get-FileHash -Path $localPath -Algorithm $algorithm | Select-Object -ExpandProperty 'Hash'
if ($remoteHash -eq $localHash) {
$matchCount++
Write-Information " Remote $remoteHash == Local $localHash"
}
else {
Write-Information " Remote $remoteHash != Local $localHash"
}
}
}
else {
Write-Information " Unable to retrieve remote hash for $localFileName. Http reponse code: $($response.StatusCode)"
}
}
if ($remoteCount -eq 0) { return $false }
if ($matchCount -eq $PackageDetail.AssociatedArtifacts.Length) { return $true }
throw "Package already deployed, but with different content."
}