diff --git a/start-vs-VisualFSharpSln.ps1 b/start-vs-VisualFSharpSln.ps1 index 9ba9aec4e8a..6ab83f91c6d 100644 --- a/start-vs-VisualFSharpSln.ps1 +++ b/start-vs-VisualFSharpSln.ps1 @@ -8,9 +8,12 @@ # 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', @@ -18,12 +21,48 @@ param( ) 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) @@ -36,7 +75,7 @@ if (-not $RoslynVersion) { if ($ini -notmatch '(?m)^InstallationVersion=(?\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) { @@ -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 @@ -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);$([Security.SecurityElement]::Escape($feeds -join ';'))" } + else { '' } + New-Item -ItemType Directory -Force (Split-Path $override) | Out-Null - "$(-join ($names | ForEach-Object { "<${_}Version>$RoslynVersion" }))" | + "$(-join ($names | ForEach-Object { "<${_}Version>$RoslynVersion" }))$sources" | Set-Content -LiteralPath $override -Encoding UTF8 } @@ -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 } & (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)`""