Skip to content
Open
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
88 changes: 72 additions & 16 deletions start-vs-VisualFSharpSln.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,61 @@
# alone when they already match, which is the common case for a hive built from this same source.
#
# -RoslynVersion forces a package version; any 5.Y.* binds identically within a minor.
# -RoslynRepo is the Roslyn repository whose packages a locally built hive Roslyn comes from (default:
# a roslyn folder next to this repository).
[CmdletBinding()]
param(
[string]$RoslynVersion,
[string]$RoslynRepo,
[string]$DevEnv,
[string]$RootSuffix = 'RoslynDev',
[string]$Solution = 'VisualFSharp.slnx',
[switch]$DryRun
)
Set-StrictMode -Version Latest; $ErrorActionPreference = 'Stop'; $root = $PSScriptRoot

# The package folders of a locally built Roslyn, checked to actually hold that version: a hive can carry
# a dev build whose packages were cleaned or never packed, and NU1101 halfway through a restore says far
# less about that than a message here does.
function Get-RoslynDevFeeds([string]$repo, [string]$version) {
if (-not $repo) { $repo = Join-Path (Split-Path $PSScriptRoot) 'roslyn' }
# NuGet resolves a relative source against each project's directory, not this one.
$repo = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($repo)
if (-not (Test-Path -LiteralPath $repo)) {
throw "$RootSuffix runs a locally built Roslyn $version, which only its own packages provide, but there is no repository at $repo; pass -RoslynRepo."
}

# NuGet fails restore (NU1301) on a local source that does not exist, so only the folders present are feeds.
$feeds = @('Shipping', 'NonShipping' | ForEach-Object { Join-Path $repo "artifacts\packages\Release\$_" } |
Where-Object { Test-Path -LiteralPath $_ })
$probe = "Microsoft.VisualStudio.LanguageServices.ExternalAccess.$version.nupkg"

if (-not ($feeds | Where-Object { Test-Path -LiteralPath (Join-Path $_ $probe) })) {
throw "$repo has no $probe; pack that Roslyn (.\Build.cmd -restore -pack -c Release) or pass -RoslynVersion to build against a published one instead."
}

$feeds
}

# A repack keeps the -dev version, and restore would serve the previous pack from the global packages
# folder, so the extracted copy of every package these feeds hold at that version is dropped first.
function Get-StaleRoslynDevPackages([string[]]$feeds, [string]$version) {
$cache = if ($env:NUGET_PACKAGES) { $env:NUGET_PACKAGES } else { Join-Path $env:USERPROFILE '.nuget\packages' }
$suffix = ".$version.nupkg"
foreach ($feed in $feeds) {
Get-ChildItem -LiteralPath $feed -Filter '*.nupkg' |
Where-Object { $_.Name.EndsWith($suffix, [StringComparison]::OrdinalIgnoreCase) } |
ForEach-Object { Join-Path $cache "$($_.Name.Substring(0, $_.Name.Length - $suffix.Length).ToLowerInvariant())\$($version.ToLowerInvariant())" } |
Where-Object { Test-Path -LiteralPath $_ }
}
}

if (-not $DevEnv) {
$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
$DevEnv = if ($env:DevEnvDir) { Join-Path $env:DevEnvDir 'devenv.exe' }
elseif (Test-Path $vswhere) { & $vswhere -latest -prerelease -property productPath 2>$null }
elseif (Test-Path -LiteralPath $vswhere) { & $vswhere -latest -prerelease -property productPath 2>$null }
}
if (-not ($DevEnv -and (Test-Path $DevEnv))) { throw 'devenv.exe not found; run from a VS Developer prompt or pass -DevEnv.' }
if (-not ($DevEnv -and (Test-Path -LiteralPath $DevEnv))) { throw 'devenv.exe not found; run from a VS Developer prompt or pass -DevEnv.' }

$ideDir = Split-Path $DevEnv
$flowed = ([xml](Get-Content -LiteralPath (Join-Path $root 'eng\Version.Details.props') -Raw)
Expand All @@ -36,7 +75,7 @@ if (-not $RoslynVersion) {
if ($ini -notmatch '(?m)^InstallationVersion=(?<v>\d+)') { throw 'No InstallationVersion in devenv.isolation.ini.' }
$hive = Join-Path $env:LOCALAPPDATA ('Microsoft\VisualStudio\{0}.0_{1}{2}' -f $Matches.v, $installationId, $RootSuffix)

$hiveRoslyn = Get-ChildItem (Join-Path $hive 'Extensions') -Recurse -Filter 'Microsoft.CodeAnalysis.dll' -ErrorAction SilentlyContinue |
$hiveRoslyn = Get-ChildItem -LiteralPath (Join-Path $hive 'Extensions') -Recurse -Filter 'Microsoft.CodeAnalysis.dll' -ErrorAction SilentlyContinue |
Where-Object { $_.DirectoryName -like '*Roslyn Language Services*' } | Select-Object -First 1

if ($hiveRoslyn) {
Expand All @@ -45,31 +84,39 @@ if (-not $RoslynVersion) {
}
else {
$dll = Join-Path $ideDir 'CommonExtensions\Microsoft\VBCSharp\LanguageServices\Microsoft.CodeAnalysis.dll'
if (-not (Test-Path $dll)) { throw "Can't detect the Roslyn version for $RootSuffix; pass -RoslynVersion." }
if (-not (Test-Path -LiteralPath $dll)) { throw "Can't detect the Roslyn version for $RootSuffix; pass -RoslynVersion." }
$target = ([System.Diagnostics.FileVersionInfo]::GetVersionInfo($dll).ProductVersion -split '\+')[0]
$source = 'shipped with the installed VS'
}
Write-Host "Roslyn in force: $target ($source)."

# A dev build's "5.12.0-dev" is no package version, so only its minor is usable - and when that is
# the minor the repo already flows, the flowed packages are the match and no override belongs here.
$targetMinor = [version](($target -split '-')[0])
$flowedMinor = [version](($flowed -split '-')[0])
if ($targetMinor.Major -eq $flowedMinor.Major -and $targetMinor.Minor -eq $flowedMinor.Minor) {
Write-Host "Repo already flows Roslyn $flowed - building against it, no override."
}
elseif ($target -match '-dev$') {
throw "$RootSuffix has a locally built Roslyn $target but the repo flows $flowed; pass -RoslynVersion with a $($targetMinor.Major).$($targetMinor.Minor).* package version to build against that minor."
# A locally built Roslyn is deployed precisely because its API surface differs from the flowed
# package - it is where an ExternalAccess contract lives before it flows - so a matching minor says
# nothing and the override belongs here whatever the repo flows. Its own packages are the only ones
# that carry the difference, so the feeds they sit in come along; without them restore cannot find
# the version at all.
if ($target -match '-dev$') { $RoslynVersion = $target }
else {
# A shipped Roslyn is a real package version, and within a minor any 5.Y.* binds identically,
# so the flowed packages are the match when the minors agree.
$targetMinor = [version](($target -split '-')[0])
$flowedMinor = [version](($flowed -split '-')[0])

if ($targetMinor.Major -eq $flowedMinor.Major -and $targetMinor.Minor -eq $flowedMinor.Minor) {
Write-Host "Repo already flows Roslyn $flowed - building against it, no override."
}
else { $RoslynVersion = $target }
}
else { $RoslynVersion = $target }
}

$feeds = $null
if ($RoslynVersion) {
$minor = [version](($RoslynVersion -split '-')[0])
if ($minor -lt [version]'5.10.0') {
throw "Roslyn $RoslynVersion is older than the 5.10 the repo's sources expect (unified ExternalAccess, #20099). Update VS or deploy a local Roslyn."
}
Write-Host "Building the F# extension against Roslyn $RoslynVersion ($DevEnv)."
if ($RoslynVersion -match '-dev$') { $feeds = Get-RoslynDevFeeds $RoslynRepo $RoslynVersion }
Write-Host "Building the F# extension against Roslyn $RoslynVersion ($DevEnv)$(if ($feeds) { ' from its own packages' })."
}

# Repoint every Roslyn package (versions set in eng/Version.Details.props) via a props file MSBuild
Expand All @@ -81,8 +128,14 @@ $names = 'MicrosoftCodeAnalysis', 'MicrosoftCodeAnalysisCompilers', 'MicrosoftCo
# whichever ran last decides what a long-lived VS session restores against.
$override = Join-Path $root 'artifacts\RoslynOverride.start-vs.props'
if ($RoslynVersion) {
# RestoreAdditionalProjectSources has to arrive through the props import rather than on the command
# line: Microsoft.FSharp.NetSdk.targets declares it TreatAsLocalProperty and appends to it.
$sources =
if ($feeds) { "<RestoreAdditionalProjectSources>`$(RestoreAdditionalProjectSources);$([Security.SecurityElement]::Escape($feeds -join ';'))</RestoreAdditionalProjectSources>" }
else { '' }

New-Item -ItemType Directory -Force (Split-Path $override) | Out-Null
"<Project><PropertyGroup>$(-join ($names | ForEach-Object { "<${_}Version>$RoslynVersion</${_}Version>" }))</PropertyGroup></Project>" |
"<Project><PropertyGroup>$(-join ($names | ForEach-Object { "<${_}Version>$RoslynVersion</${_}Version>" }))$sources</PropertyGroup></Project>" |
Set-Content -LiteralPath $override -Encoding UTF8
}

Expand All @@ -102,11 +155,14 @@ if ($RoslynVersion) { $vars.CustomAfterMicrosoftCommonProps = $override }
$saved = @{}; foreach ($k in $vars.Keys) { $saved[$k] = [Environment]::GetEnvironmentVariable($k) }
try {
foreach ($k in $vars.Keys) { Set-Item -LiteralPath "Env:\$k" -Value $vars[$k] }
$stale = if ($feeds) { @(Get-StaleRoslynDevPackages $feeds $RoslynVersion) } else { @() }
if ($DryRun) {
if ($RoslynVersion) { Write-Host "DryRun: wrote $override" } else { Write-Host 'DryRun: no override needed' }
$stale | ForEach-Object { Write-Host "DryRun: would remove $_" }
Write-Host "Would restore, then open $Solution in $DevEnv; F5 deploys into and launches $RootSuffix"
return
}
$stale | ForEach-Object { Remove-Item -LiteralPath $_ -Recurse -Force }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Keeping the shared cache. Only <id>/<version> entries at the -dev version are removed, and only for packages the local Roslyn feeds hold at that version. No published package carries that version, so nothing restored from a real feed is affected. The only other readers are builds against this same local Roslyn pack, and those would pick up the stale copy anyway.

A separate cache per launch would re-download every package the repo restores on each launch, and the launched VS would keep that cache for all its builds. Arcade deliberately keeps dev builds on the global cache to avoid that cost (InitializeNuGetPackageCachePath in eng/common/tools.ps1).

& (Join-Path $root 'Restore.cmd')
if ($LASTEXITCODE) { throw "Restore failed for Roslyn $RoslynVersion; try another 5.$($minor.Minor).* build via -RoslynVersion." }
Start-Process $DevEnv "`"$(Join-Path $root $Solution)`""
Expand Down
Loading