Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 2 additions & 60 deletions eng/Migrate-Package.ps1 → eng/scripts/Migrate-Package.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -43,65 +43,7 @@ Param (
$DestinationRepoPath # Examples: 'C:/source/repos/maintenance-packages', '/home/user/maintenance-packages'.
)

#### Functions ####

Function Write-Color
{
Param (
[ValidateNotNullOrWhiteSpace()]
[string] $newColor
)

$oldColor = $host.UI.RawUI.ForegroundColor
$host.UI.RawUI.ForegroundColor = $newColor

If ($args)
{
Write-Output $args
}
Else
{
$input | Write-Output
}

$host.UI.RawUI.ForegroundColor = $oldColor
}

Function VerifyPathOrExit
{
Param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[string]
$path
)

If (-Not (Test-Path -Path $path))
{
Write-Error "The path does not exist: $path"
Exit
}
ElseIf (-Not ($path -match "^[a-zA-Z0-9\.\-_:/ ]+$"))
{
Write-Error "The path will not work with Git. Avoid backslashes: $path"
Exit
}

Write-Color green "The path is valid: $path"
}

Function Execute-Command
{
Param (
[Parameter(Mandatory=$true)]
[ValidateNotNullOrWhiteSpace()]
[string]
$command
)

Write-Color cyan "Executing: $command"
Invoke-Expression $command
}
. $PSScriptRoot/Shared.ps1

#### Execution ####

Expand Down Expand Up @@ -203,4 +145,4 @@ Execute-Command "git checkout $WorkingBranchName"
# Bring in the changes that were migrated to the empty branch
Execute-Command "git merge --allow-unrelated-histories $NoHistoryBranchName"

Write-Color green "Finished!"
Write-Color green "Finished!"
55 changes: 55 additions & 0 deletions eng/scripts/Shared.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Licensed to the .NET Foundation under one or more agreements.
# The .NET Foundation licenses this file to you under the MIT license.

#### Functions ####

Function Write-Color {
Param (
[ValidateNotNullOrWhiteSpace()]
[string] $newColor
)

$oldColor = $host.UI.RawUI.ForegroundColor
$host.UI.RawUI.ForegroundColor = $newColor

If ($args) {
Write-Output $args
}
Else {
$input | Write-Output
}

$host.UI.RawUI.ForegroundColor = $oldColor
}

Function VerifyPathOrExit {
Param (
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[string]
$path
)

If (-Not (Test-Path -Path $path)) {
Write-Error "The path does not exist: $path"
Exit
}
ElseIf (-Not ($path -match "^[a-zA-Z0-9\.\-_:/ ]+$")) {
Write-Error "The path will not work with Git. Avoid backslashes: $path"
Exit
}

Write-Color green "The path is valid: $path"
}

Function Execute-Command {
Param (
[Parameter(Mandatory = $true)]
[ValidateNotNullOrWhiteSpace()]
[string]
$command
)

Write-Color cyan "Executing: $command"
Invoke-Expression $command
}
159 changes: 159 additions & 0 deletions eng/scripts/Update-Packages.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# Licensed to the .NET Foundation under one or more agreements.
# The .NET Foundation licenses this file to you under the MIT license.

###########
# Imports #
###########

. $PSScriptRoot/Shared.ps1

#############
# Functions #
#############

Function Get-NewVersion
{
Param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrWhiteSpace()]
[string]$ElementValue,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrWhiteSpace()]
[int]$NegativePosition
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why negative position and not just index?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sometimes I'm in need of some spice in my code.

I guess I can change it to index, yeah. I can still do the out of bounds check.

)

If ($NegativePosition -ge 0)
{
Write-Error "The position number '$NegativePosition' should have been negative." -ErrorAction Stop
}

$components = $ElementValue -Split '\.'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason not to use Version class?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that's the one. It's meant to deal with the versions as numbers, it handles 2-4 parts with no suffix. I think that'd work here. Might save even having to pass any sort of extra info if we're always modifying the same offset.


If (($NegativePosition * -1) -gt $components.Length)
{
Write-Error "Version number '$ElementValue' is out of range." -ErrorAction Stop
}

$components[$NegativePosition] = [int]$components[$NegativePosition] + 1

Return [string]::Join('.', $components)
}

Function Get-AssemblyNameLowerCase
{
Param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrWhiteSpace()]
[string]$MSBuildVersionFilePath
)

$assemblyName = [System.IO.Path]::GetFileNameWithoutExtension($MSBuildVersionFilePath)

If ($assemblyName -eq "Versioning")
{
$srcFolderPath = Join-Path -Path ([System.IO.Path]::GetDirectoryName($MSBuildVersionFilePath)) -ChildPath "src"
$actualCsproj = Get-ChildItem -Path $srcFolderPath -Filter "*.*proj" -Recurse | Select-Object -First 1
$assemblyName = [System.IO.Path]::GetFileNameWithoutExtension($actualCsproj.FullName)
}

Return $assemblyName.ToLowerInvariant()
}

Function Save-Project
{
Param(
[Parameter(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
[System.Xml.Linq.XDocument]
$XDoc,
[Parameter(Mandatory = $true)]
[ValidateNotNullOrWhiteSpace()]
[string]$File
)

# Ideally, we would've simply used:
# $XDoc.Save($file, [System.Xml.Linq.SaveOptions]::DisableFormatting)
# Unforutnately, it does not work as expected in PowerShell. Unlike in C#, the xml declaration
# is still getting added at the top even though we specified to disable formatting.
# Workaround: Use an XmlWriter to serialize into a string the XML without the xml declaration.
# Also, we need to remove the trailing new line that is added by the XmlWriter.

$stringWriter = New-Object System.IO.StringWriter
$xmlWriterSettings = New-Object System.Xml.XmlWriterSettings
$xmlWriterSettings.OmitXmlDeclaration = $true

$xmlWriter = [System.Xml.XmlWriter]::Create($stringWriter, $xmlWriterSettings)
$XDoc.WriteTo($xmlWriter)
$xmlWriter.Close()

$output = $stringWriter.ToString()

If ($output.EndsWith("`r`n"))
{
$output = $output.Substring(0, $output.Length - 2)
}
ElseIf ($output.EndsWith("`n"))
{
$output = $output.Substring(0, $output.Length - 1)
}
Comment on lines +91 to +98
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe [System.Xml.NewLineHandling]::None will fix this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I just realized that it's VS Code that adds the newline whenever I open the file, not the xml API. So I don't need the NewLineHandling nor these if/else.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm they're still getting added even if I open the file in notepad, and even if I add that NewLineHandline.None setting. Oh well, I'll have to keep the if/else.

Copy link
Member

@ericstj ericstj Apr 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This worked for me:

[Reflection.Assembly]::LoadWithPartialName("System.Xml.Linq")
$xdoc = [System.Xml.Linq.XDocument]::Load("in.csproj", [System.Xml.Linq.LoadOptions]::PreserveWhitespace)\

$xmlWriterSettings = New-Object System.Xml.XmlWriterSettings
$xmlWriterSettings.OmitXmlDeclaration = $true
$xmlWriterSettings.NewLineHandling = [System.Xml.NewLineHandling]::None

$xmlWriter = [System.Xml.XmlWriter]::Create("out.csproj", $xmlWriterSettings)
$xdoc.WriteTo($xmlWriter)
$xmlWriter.Close()

It was able to round trip the project file produced by .NET new without any changes.

I also found that the built-in XML support in PS worked well as shown in those other scripts.


Set-Content -Path $File -Value $output
}

###############
# Main Script #
###############

$RepoRootPath = Join-Path -Path $PSScriptRoot -ChildPath "../.."

$versioningPropsFilePaths = @((Join-Path $RepoRootPath "src" "System.Runtime.CompilerServices.Unsafe" "Versioning.props")) -as [string[]]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's not hard-code special cases like this. It's something we need to remember to do. Just evaluate props/targets as well. I think your logic for IsPackable will do the right thing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you mean. Okay sure.

$csprojFilePaths = (Get-ChildItem -Path (Join-Path $RepoRootPath "src\*\src") -Filter "*.csproj" -Recurse | ForEach-Object { $_.FullName }) -as [string[]]

$msBuildVersionFilePaths = [System.Collections.Generic.List[string]]::new()
$msBuildVersionFilePaths.AddRange($versioningPropsFilePaths)
$msBuildVersionFilePaths.AddRange($csprojFilePaths)

foreach ($file in $msBuildVersionFilePaths)
{
$xdoc = [System.Xml.Linq.XDocument]::Load($file, [System.Xml.Linq.LoadOptions]::PreserveWhitespace)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you wanted to try it there is an MSBuild construction API https://learn.microsoft.com/en-us/dotnet/api/microsoft.build.construction. Not sure it adds much value here since we're just modifying existing elements, but it might help with the save issues you were facing.

There's also native support for XML in powershell which might also be an option. I see some folks using it in dotnet repos including to modify projects: https://github.com/search?q=org%3Adotnet+%2F%5C%5Bxml%5C%5D%2F+path%3A*.ps1&type=code


$propertyGroup = $xdoc.Root.Elements("PropertyGroup") |
Where-Object { $null -ne $_.Element("IsPackable") -And $_.Element("IsPackable").Value -eq "true" } |
Select-Object -First 1

if ($propertyGroup)
{
$assemblyNameLowerCase = Get-AssemblyNameLowerCase -MSBuildVersionFilePath $file
$response = Invoke-RestMethod -Uri "https://api.nuget.org/v3/registration5-semver1/$assemblyNameLowerCase/index.json" -Method Get
$latestNuGetVersion = $response.items[0].upper

$conditionedVersionPrefix = $propertyGroup.Elements("VersionPrefix") |
Where-Object { $_.Attribute("Condition") -and $_.Attribute("Condition").Value -eq "`'`$(IsPackable)`' == 'true'" } |
Select-Object -First 1

if ($null -ne $conditionedVersionPrefix -and $conditionedVersionPrefix.GetType() -eq [System.Xml.Linq.XElement])
{
if ($latestNuGetVersion -eq $conditionedVersionPrefix.Value)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this is to handle a case where we run this script and didn't publish all the previously shipped packages? Or someone versioned them separately? Walk me though the scenario where we run this and don't want to reset packages. (consider adding as comment). Silently skipping work like this makes me nervous because it creates a way for us to miss things.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Take the current case of System.Runtime.CompilerServices.Unsafe, it's set to IsPackable=true at the moment.

We want this script to only update the packages after they're pushed to nuget.org.

Let's say we already pushed the packages, the script runs, and it will find that S.R.CS.Unsafe is enabled. The script will analyze this project.

So now we are going to check if the version of the freshly pushed package to nuget.org matches the version that we have in the VersionPrefix property that has the IsPackable condition. At that moment, the script would confirm this is one of those packages that need to be updated, and will bump all the version numbers and set IsPackable to false.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I understand that. Let's suppose we haven't yet pushed the packages to NuGet.org - in that case why are we even running the script?
Or let's suppose someone manually incremented the version of the package - is that something we want folks to do?

I'm just trying to understand why we'd ever want to skip versioning a package that's enabled. If you really wish to keep this then introduce logging that says skipping versioning package ID because it's version is higher than that on nuget.org - and make sure the condition you check matches that (equals seems wrong).

{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For all of the below - do we want to fail if any are not found, or do any comparisons against latestNuGetVersion?

foreach ($isPackableElement in $propertyGroup.Elements("IsPackable"))
{
$isPackableElement.Value = "false"
}
foreach ($versionPrefixElement in $propertyGroup.Elements("VersionPrefix"))
{
$versionPrefixElement.Value = Get-NewVersion -ElementValue $versionPrefixElement.Value -NegativePosition -1
}
foreach ($assemblyVersionElement in $propertyGroup.Elements("AssemblyVersion"))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you handle the properties that are meant not to change?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am only looking for the <PropertyGroup> that contains the definition of <IsPackable>.

Any property that isn't meant to change is supposed to not be part of that same property group. That was something suggested by @ViktorHofer when we were initially working on all of this. Examples:

<AssemblyVersion>4.0.2.0</AssemblyVersion> <!-- NO-INCREMENT: This version is frozen for netstandard2.0. -->

<AssemblyVersion>4.6.1.6</AssemblyVersion> <!-- NO-INCREMENT: This version is frozen for .NET Standard and .NETCoreApp because the assembly ships inbox. -->

Those aren't getting modified.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a very unusual requirement and not enforced. Folks "have to know". At least add comments about why that's the case here.

If you wanted to also safeguard and honor that comment you could do something like (assemblyVersionElement.NextNode() as XComment)?.Value.?StartsWith("NO-INCREMENT") to check for a sibling comment with the token word. You could do that coinsistently.

{
$assemblyVersionElement.Value = Get-NewVersion -ElementValue $assemblyVersionElement.Value -NegativePosition -2
}
foreach ($packageValidationBaselineVersionElement in $propertyGroup.Elements("PackageValidationBaselineVersion"))
{
$packageValidationBaselineVersionElement.Value = Get-NewVersion -ElementValue $packageValidationBaselineVersionElement.Value -NegativePosition -1
}

Save-Project -XDoc $xdoc -File $file
}
}
}
}