diff --git a/.github/agents/compiler-perf-investigator.md b/.github/agents/compiler-perf-investigator.md index 716821d5ae9..5ad3c9477d0 100644 --- a/.github/agents/compiler-perf-investigator.md +++ b/.github/agents/compiler-perf-investigator.md @@ -40,7 +40,7 @@ These are **general investigation instructions** for this agent, a template for ### 1. Preparation - **Setup:** Clone/generate repo/snippet/etc. - **Clear old config:** Remove `global.json` unless needed. -- **Prepare local compiler:** Use `PrepareRepoForRegressionTesting.fsx` and absolute env paths. +- **Prepare local compiler:** Build via `dotnet fsi /eng/scripts/BuildWithLocalFSharp.fsx --build-script ''` (sets the local-compiler + FSharp.Core shim env). ### 2. Experiment Matrix diff --git a/UseLocalCompiler.Directory.Build.props b/UseLocalCompiler.Directory.Build.props index 8bd796bfe90..6cfa1da98bc 100644 --- a/UseLocalCompiler.Directory.Build.props +++ b/UseLocalCompiler.Directory.Build.props @@ -7,39 +7,51 @@ False - Release + $(MSBuildThisFileDirectory) + - $(MSBuildThisFileDirectory) - - true + + true $([System.IO.Path]::GetDirectoryName($(DOTNET_HOST_PATH))) $([System.IO.Path]::GetFileName($(DOTNET_HOST_PATH))) - - $(LocalFSharpCompilerPath)/artifacts/bin/fsc/$(LocalFSharpCompilerConfiguration)/$(FSharpNetCoreProductTargetFramework)/fsc.dll - $(LocalFSharpCompilerPath)/artifacts/bin/fsc/$(LocalFSharpCompilerConfiguration)/$(FSharpNetCoreProductTargetFramework)/fsc.dll - False True - - - $(LocalFSharpCompilerPath)/artifacts/bin/fsc/$(LocalFSharpCompilerConfiguration)/$(FSharpNetCoreProductTargetFramework) + $(LocalFSharpBuildBinPath)/fsc.dll + $(LocalFSharpBuildBinPath)/fsc.dll $(LocalFSharpBuildBinPath)/FSharp.Build.dll $(LocalFSharpBuildBinPath)/Microsoft.FSharp.Targets $(LocalFSharpBuildBinPath)/Microsoft.FSharp.NetSdk.props $(LocalFSharpBuildBinPath)/Microsoft.FSharp.NetSdk.targets $(LocalFSharpBuildBinPath)/Microsoft.FSharp.Overrides.NetSdk.targets + + $(OtherFlags) --nowarn:75 --times + + + + + $(RegressionLocalCoreVersion) + true + <_FSharpCoreLibraryPacksFolder>$([MSBuild]::ValueOrDefault('$(RegressionLocalCorePackagesDir)', '$(MSBuildThisFileDirectory)library-packs')) - + + + + + + + true + + diff --git a/UseLocalCompiler.Directory.Build.targets b/UseLocalCompiler.Directory.Build.targets new file mode 100644 index 00000000000..0779e90aa02 --- /dev/null +++ b/UseLocalCompiler.Directory.Build.targets @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/azure-pipelines-PR.yml b/azure-pipelines-PR.yml index 65f45277382..ff6f4603975 100644 --- a/azure-pipelines-PR.yml +++ b/azure-pipelines-PR.yml @@ -583,13 +583,19 @@ stages: condition: succeeded() - pwsh: | - # Stage UseLocalCompiler props and TargetFrameworks.props together + # Stage the props and the locally built FSharp.Core so regression jobs restore it the SDK way. + # Arcade packs FSharp.Core into a `Shipping` leaf whose parent varies by layout, so search for it. $stagingDir = "$(Build.SourcesDirectory)/UseLocalCompilerPropsStaging" - New-Item -ItemType Directory -Force -Path $stagingDir | Out-Null + $packsDir = Join-Path $stagingDir "library-packs" + New-Item -ItemType Directory -Force -Path $packsDir | Out-Null Copy-Item "$(Build.SourcesDirectory)/UseLocalCompiler.Directory.Build.props" -Destination $stagingDir Copy-Item "$(Build.SourcesDirectory)/eng/TargetFrameworks.props" -Destination $stagingDir + $core = Get-ChildItem "$(Build.SourcesDirectory)/artifacts/packages/Release" -Recurse -Filter "FSharp.Core.*.nupkg" | + Where-Object { $_.Name -notlike "*.symbols.nupkg" -and $_.Directory.Name -eq "Shipping" } | Select-Object -First 1 + if (-not $core) { Write-Host "##[error]FSharp.Core.*.nupkg not found under artifacts/packages/Release/**/Shipping"; exit 1 } + Copy-Item $core.FullName -Destination $packsDir Write-Host "Staged files for UseLocalCompilerProps artifact:" - Get-ChildItem $stagingDir -Name + Get-ChildItem $stagingDir -Recurse -Name displayName: Stage UseLocalCompiler props files - task: PublishPipelineArtifact@1 @@ -749,6 +755,7 @@ stages: commit: bbe2dec4d0379b5d7d0480997858c30d442fbb42 buildScript: dotnet build -bl displayName: UMX_Slow_Repro + expectLocalCore: true - repo: fsprojects/FSharpPlus commit: f614035b75922aba41ed6a36c2fc986a2171d2b8 buildScript: build.cmd @@ -763,6 +770,7 @@ stages: commit: 2648efe buildScript: dotnet build tests/FSharpPlus.Tests/FSharpPlus.Tests.fsproj -c Release -bl displayName: FsharpPlus_NET10_Build_Lib_Tests + expectLocalCore: true # remove this before merging - repo: fsprojects/FSharpPlus commit: 2648efe @@ -814,10 +822,18 @@ stages: commit: 6ddc28d46f81447eacb241b96e16ce693b210c96 buildScript: dotnet build Prime.sln --configuration Release displayName: Prime_Build + expectLocalCore: true - repo: bryanedds/Nu commit: e81e00a464b9d35d272f61708a1a0bfbf487b6d5 buildScript: dotnet build Nu.sln --configuration Release displayName: Nu_Build + expectLocalCore: true + # Explicit FSharp.Core PackageReference mixed with one implicit test project; the trigger for #20059 (Lanayx/Oxpecker#20065). + - repo: Lanayx/Oxpecker + commit: cb7e4b83e3f2aba7ded46b17a36d1aea8c13c292 + buildScript: dotnet build Oxpecker.slnx -c Release + displayName: Oxpecker_Build + expectLocalCore: true # Design-time provider packaging oracle. Pin d8aba70 (pre-workaround base of FSharp.Data.GraphQL#583): # it uses the bare IsFSharpDesignTimeProvider gesture, so the client pack drops the provider without this fix. # Linux-only: the $PWD local feed and the grep content assertion need bash. (nupkg entry names are stored diff --git a/docs/regression-testing-pipeline.md b/docs/regression-testing-pipeline.md index 5e3013bfad5..7c0518f06ea 100644 --- a/docs/regression-testing-pipeline.md +++ b/docs/regression-testing-pipeline.md @@ -25,8 +25,8 @@ The regression testing logic is implemented as a reusable Azure DevOps template 1. **Build F# Compiler**: The `EndToEndBuildTests` job builds the F# compiler and publishes required artifacts 2. **Matrix Execution**: For each library in the test matrix (running in parallel): - Checkout the third-party repository at a specific commit - - Install appropriate .NET SDK version using the repository's `global.json` - - Setup `Directory.Build.props` to import `UseLocalCompiler.Directory.Build.props` + - Pin `global.json` to the exact SDK that built the local compiler + - Inject `UseLocalCompiler.Directory.Build.props` via `CustomAfterDirectoryBuildProps` - Build the library using its standard build script - Publish MSBuild binary logs for analysis 3. **Report Results**: Success/failure status is reported with build logs for diagnosis @@ -68,8 +68,9 @@ To add a new library to the test matrix, update the template invocation in `azur Each test matrix entry requires: - **repo**: GitHub repository in `owner/name` format - **commit**: Specific commit SHA for reproducible results -- **buildScript**: Build script to execute (e.g., `build.cmd`, `build.sh`) +- **buildScript**: Build command to execute — a `dotnet ...` command or a script file (`build.cmd`/`build.sh`); `;;` separates commands run sequentially, fail-fast - **displayName**: Human-readable name for the job +- **expectLocalCore** (optional): set `true` when the repo has projects that take the implicit `FSharp.Core`; the job then fails unless the locally built FSharp.Core is actually restored — a tripwire for a silently broken shim ## Pipeline Configuration @@ -82,10 +83,10 @@ Regression tests run automatically as part of PR builds when: ### Build Environment -- **OS**: Windows (using `$(WindowsMachineQueueName)`) +- **OS**: Windows by default (`$(WindowsMachineQueueName)`); matrix entries can override to Linux. The scripts are OS-agnostic - **Pool**: Standard public build pool (`$(DncEngPublicBuildPool)`) -- **Timeout**: 60 minutes per regression test job -- **.NET SDK**: Automatically detects and installs SDK version from each repository's `global.json` +- **Timeout**: 120 minutes per regression test job +- **.NET SDK**: Each test repo's `global.json` is pinned to the SDK that built the local compiler, so `fsc.dll` and its host runtime line up ### Artifacts @@ -108,26 +109,19 @@ When a regression test fails: ### Local Testing -To test a library locally with your F# compiler build: +To reproduce a regression locally, on any OS, without editing the library: -1. Build the F# compiler: `.\Build.cmd -c Release -pack` - -2. In the third-party library directory, create a `Directory.Build.props`: - ```xml - - - +1. Build the compiler and pack FSharp.Core in your `dotnet/fsharp` checkout: `./build.sh -c Release -pack` (`Build.cmd` on Windows). +2. Clone the library at the failing commit and build it against your local build: ``` - -3. Update the `LocalFSharpCompilerPath` in `UseLocalCompiler.Directory.Build.props` to point to your F# repository. - -4. Set environment variables: - ```cmd - set LoadLocalFSharpBuild=true - set LocalFSharpCompilerConfiguration=Release + git clone --recursive https://github.com//.git TestRepo + cd TestRepo && git checkout + # If TestRepo's global.json pins a different SDK, align sdk.version with /global.json + # (allowPrerelease: true, rollForward: disable) — the clone is disposable, as in CI. + dotnet fsi /eng/scripts/BuildWithLocalFSharp.fsx --build-script '' ``` -5. Run the library's build script. +`BuildWithLocalFSharp.fsx` runs the same command CI runs, from the current directory, without touching the repo's sources. Add `--verify` to fail unless every project consumes the local FSharp.Core; the script header lists the other options. Because the local package keeps a fixed `-dev` version, the script evicts it from the global NuGet cache before each run so a rebuild is never served stale; pass `--nuget-packages ` to use an isolated cache when running several builds concurrently or against a repo that redirects its packages folder. ## Best Practices @@ -148,20 +142,7 @@ To test a library locally with your F# compiler build: ### UseLocalCompiler.Directory.Build.props -This MSBuild props file configures projects to use the locally built F# compiler instead of the SDK version. Key settings: - -- `LocalFSharpCompilerPath`: Points to the F# compiler artifacts -- `DotnetFscCompilerPath`: Path to the fsc.dll compiler -- `DisableImplicitFSharpCoreReference`: Ensures local FSharp.Core is used - -### Path Handling - -The pipeline dynamically updates paths in the props file using PowerShell: -```powershell -$content -replace 'LocalFSharpCompilerPath.*MSBuildThisFileDirectory.*', 'LocalFSharpCompilerPath>$(Pipeline.Workspace)/FSharpCompiler<' -``` - -This ensures the correct path is used in the Azure DevOps environment. +This MSBuild props file redirects projects to the locally built F# compiler (and, for the matrix, the locally built FSharp.Core) instead of the SDK version. It is organised into gates so it can be injected into unmodified repos as well as imported directly by in-repo tests — see the `Gate 1/2/3` comments in the file. Its companion `UseLocalCompiler.Directory.Build.targets` is injected via `CustomAfterDirectoryBuildTargets` (after the target repo's project body) so the local FSharp.Core version wins over the repo's own reference, whether implicit, an explicit `PackageReference Include`, a `PackageReference Update`, or a central `PackageVersion` (Central Package Management). ## Future Enhancements diff --git a/eng/scripts/BuildWithLocalFSharp.fsx b/eng/scripts/BuildWithLocalFSharp.fsx new file mode 100644 index 00000000000..d01aaeca138 --- /dev/null +++ b/eng/scripts/BuildWithLocalFSharp.fsx @@ -0,0 +1,118 @@ +// Build an unmodified repo with this checkout's F# compiler and FSharp.Core, on any OS (needs only the .NET SDK). +// dotnet fsi /eng/scripts/BuildWithLocalFSharp.fsx --build-script "dotnet build MySolution.sln" +// Prerequisite: build this checkout with `-c Release -pack`. + +open System +open System.IO +open System.Diagnostics + +let fail (msg: string) : 'a = eprintfn "ERROR: %s" msg; exit 1 + +let opts = System.Collections.Generic.Dictionary(StringComparer.OrdinalIgnoreCase) + +let rec parseArgs = function + | (key: string) :: value :: rest when key.StartsWith "--" && not (value.StartsWith "--") -> + opts.[key.Substring 2] <- value + parseArgs rest + | key :: rest when key.StartsWith "--" -> + opts.[key.Substring 2] <- "true" + parseArgs rest + | _ :: rest -> parseArgs rest + | [] -> () + +fsi.CommandLineArgs |> Array.tail |> Array.toList |> parseArgs + +let tryOpt k = match opts.TryGetValue k with | true, v -> Some v | _ -> None +let opt k d = defaultArg (tryOpt k) d + +let fsharpRoot = opt "fsharp-root" (Path.GetFullPath(Path.Combine(__SOURCE_DIRECTORY__, "..", ".."))) +let configuration = opt "configuration" "Release" +let compilerPath = opt "compiler-path" fsharpRoot +let props = opt "props" (Path.Combine(fsharpRoot, "UseLocalCompiler.Directory.Build.props")) +let targets = opt "targets" (Path.Combine(fsharpRoot, "UseLocalCompiler.Directory.Build.targets")) +let corePackagesDir = opt "core-packages-dir" (Path.Combine(compilerPath, "artifacts", "packages", configuration)) +let repoDir = opt "repo-dir" (Directory.GetCurrentDirectory()) +let buildScript = match tryOpt "build-script" with Some s -> s | None -> fail "--build-script is required" +let verify = (tryOpt "verify").IsSome + +if not (File.Exists props) then fail (sprintf "props file not found: %s" props) +if not (File.Exists targets) then fail (sprintf "targets file not found: %s" targets) +if not (Directory.Exists corePackagesDir) then + fail (sprintf "FSharp.Core package folder not found: %s (build the compiler with `-c %s -pack`)" corePackagesDir configuration) + +let nupkg = + // Arcade routes FSharp.Core to a `Shipping` leaf that varies by layout (Release/Shipping locally, + // Dependency/Shipping on CI), so search recursively and prefer that folder, then newest. + Directory.GetFiles(corePackagesDir, "FSharp.Core.*.nupkg", SearchOption.AllDirectories) + |> Array.filter (fun f -> not (f.EndsWith(".symbols.nupkg", StringComparison.OrdinalIgnoreCase))) + |> Array.sortByDescending (fun f -> Path.GetFileName(Path.GetDirectoryName f) = "Shipping", File.GetLastWriteTimeUtc f) + |> Array.tryHead + |> Option.defaultWith (fun () -> fail (sprintf "no FSharp.Core.*.nupkg under %s" corePackagesDir)) + +let version = Path.GetFileNameWithoutExtension(nupkg).Substring("FSharp.Core.".Length) + +let setEnv k v = Environment.SetEnvironmentVariable(k, v) +setEnv "LoadLocalFSharpBuild" "True" +setEnv "LocalFSharpCompilerPath" compilerPath +setEnv "LocalFSharpCompilerConfiguration" configuration +setEnv "CustomAfterDirectoryBuildProps" props +setEnv "CustomAfterDirectoryBuildTargets" targets +setEnv "RegressionLocalCore" "true" +setEnv "RegressionLocalCoreVersion" version +setEnv "RegressionLocalCorePackagesDir" corePackagesDir +tryOpt "nuget-packages" |> Option.iter (setEnv "NUGET_PACKAGES") + +// NuGet caches by id+version, so a repacked same-version local FSharp.Core would be served stale; evict it first. +let globalPackages = + match Environment.GetEnvironmentVariable "NUGET_PACKAGES" with + | null | "" -> Path.Combine(Environment.GetFolderPath Environment.SpecialFolder.UserProfile, ".nuget", "packages") + | p -> p +let cachedCore = Path.Combine(globalPackages, "fsharp.core", version) +if Directory.Exists cachedCore then + try Directory.Delete(cachedCore, true) + with e -> eprintfn "WARN: could not evict cached %s: %s" cachedCore e.Message + +printfn "Local F# compiler: %s (%s)" compilerPath configuration +printfn "Local FSharp.Core: %s from %s" version corePackagesDir + +let run (command: string) = + let psi = ProcessStartInfo(WorkingDirectory = repoDir, UseShellExecute = false) + let launch = + if OperatingSystem.IsWindows() then + psi.FileName <- "cmd.exe" + psi.ArgumentList.Add "/c" + if command.StartsWith("dotnet", StringComparison.OrdinalIgnoreCase) then command else ".\\" + command + else + psi.FileName <- "/bin/bash" + psi.ArgumentList.Add "-c" + // Escape bare ';' so MSBuild's `-t:Build;Test` stays one argument, and run non-dotnet scripts + // through bash instead of `chmod +x` so the checked-out repo is never modified. + let escaped = command.Replace(";", "\\;") + if command.StartsWith("dotnet", StringComparison.OrdinalIgnoreCase) then escaped else "bash " + escaped + psi.ArgumentList.Add launch + printfn "==> %s" command + use p = Process.Start psi + p.WaitForExit() + p.ExitCode + +for cmd in buildScript.Split([| ";;" |], StringSplitOptions.RemoveEmptyEntries ||| StringSplitOptions.TrimEntries) do + let code = run cmd + if code <> 0 then fail (sprintf "build command failed with exit code %d" code) + +if verify then + // Fail if any project resolved a non-local FSharp.Core; match the exact quoted identity so a longer + // prerelease can't satisfy a prefix. + let rx = System.Text.RegularExpressions.Regex("\"FSharp\\.Core/([^\"]+)\"") + let options = EnumerationOptions(RecurseSubdirectories = true, IgnoreInaccessible = true) + let mutable usedLocal = false + let others = System.Collections.Generic.SortedSet() + for f in Directory.EnumerateFiles(repoDir, "project.assets.json", options) do + let text = try File.ReadAllText f with _ -> "" + for m in rx.Matches text do + if m.Groups.[1].Value = version then usedLocal <- true + else others.Add m.Groups.[1].Value |> ignore + if others.Count > 0 then + fail (sprintf "expected local FSharp.Core %s but some projects resolved: %s" version (String.Join(", ", others))) + if not usedLocal then + fail (sprintf "expected local FSharp.Core %s in project.assets.json but found none; built against a different FSharp.Core" version) + printfn "Verified: local FSharp.Core %s was consumed." version diff --git a/eng/scripts/PrepareRepoForRegressionTesting.fsx b/eng/scripts/PrepareRepoForRegressionTesting.fsx deleted file mode 100644 index e1df77bcb45..00000000000 --- a/eng/scripts/PrepareRepoForRegressionTesting.fsx +++ /dev/null @@ -1,110 +0,0 @@ -/// Script to inject UseLocalCompiler.Directory.Build.props import into a third-party repository's Directory.Build.props -/// Usage: dotnet fsi PrepareRepoForRegressionTesting.fsx - -open System -open System.IO -open System.Xml - -let propsFilePath = "Directory.Build.props" - -let useLocalCompilerPropsPath = - let args = Environment.GetCommandLineArgs() - // When running with dotnet fsi, args are: [0]=dotnet; [1]=fsi.dll; [2]=script.fsx; [3...]=args - let scriptArgs = args |> Array.skipWhile (fun a -> not (a.EndsWith(".fsx"))) |> Array.skip 1 - if scriptArgs.Length > 0 then - scriptArgs.[0] - else - failwith "Usage: dotnet fsi PrepareRepoForRegressionTesting.fsx " - -printfn "PrepareRepoForRegressionTesting.fsx" -printfn "===================================" -printfn "UseLocalCompiler props path: %s" useLocalCompilerPropsPath - -if not (File.Exists(useLocalCompilerPropsPath)) then - failwithf "UseLocalCompiler.Directory.Build.props not found at: %s" useLocalCompilerPropsPath - -printfn "✓ UseLocalCompiler.Directory.Build.props found" - -let absolutePropsPath = - Path.GetFullPath(useLocalCompilerPropsPath).Replace("\\", "/") -printfn "Absolute path: %s" absolutePropsPath - -if File.Exists(propsFilePath) then - printfn "Directory.Build.props exists, modifying it..." - - let doc = XmlDocument() - doc.PreserveWhitespace <- true - doc.Load(propsFilePath) - - let projectElement = doc.SelectSingleNode("/Project") - if isNull projectElement then - failwith "Could not find Project element in Directory.Build.props" - - let xpath = "//Import[contains(@Project, 'UseLocalCompiler.Directory.Build.props')]" - let existingImport = doc.SelectSingleNode(xpath) - - if isNull existingImport then - let importElement = doc.CreateElement("Import") - importElement.SetAttribute("Project", absolutePropsPath) - - if projectElement.HasChildNodes then - projectElement.InsertBefore(importElement, projectElement.FirstChild) |> ignore - else - projectElement.AppendChild(importElement) |> ignore - - let newline = doc.CreateTextNode("\n ") - projectElement.InsertAfter(newline, importElement) |> ignore - - doc.Save(propsFilePath) - printfn "✓ Added UseLocalCompiler import to Directory.Build.props" - else - printfn "✓ UseLocalCompiler import already exists" - - let otherFlagsWithTimes = doc.SelectSingleNode("//OtherFlags[contains(text(), '--times')]") - - if isNull otherFlagsWithTimes then - let propertyGroup = doc.CreateElement("PropertyGroup") - let otherFlags = doc.CreateElement("OtherFlags") - otherFlags.InnerText <- "$(OtherFlags) --nowarn:75 --times" - propertyGroup.AppendChild(otherFlags) |> ignore - - let importNode = doc.SelectSingleNode(xpath) - - // PreserveWhitespace=true causes XML DOM to keep text nodes (newlines/indentation) between elements; - // skip past the whitespace text node after the import to position the PropertyGroup correctly - let nodeAfterImport = - if not (isNull importNode) && not (isNull importNode.NextSibling) && importNode.NextSibling.NodeType = XmlNodeType.Text then - importNode.NextSibling - else - null - - if not (isNull nodeAfterImport) then - projectElement.InsertAfter(propertyGroup, nodeAfterImport) |> ignore - else - projectElement.InsertAfter(propertyGroup, importNode) |> ignore - - let newlineAfter = doc.CreateTextNode("\n ") - projectElement.InsertAfter(newlineAfter, propertyGroup) |> ignore - - doc.Save(propsFilePath) - printfn "✓ Added --times flag to OtherFlags" - else - if not (otherFlagsWithTimes.InnerText.Contains("--nowarn:75")) then - otherFlagsWithTimes.InnerText <- otherFlagsWithTimes.InnerText.Replace("--times", "--nowarn:75 --times") - doc.Save(propsFilePath) - printfn "✓ Added --nowarn:75 to existing OtherFlags" - else - printfn "✓ --times and --nowarn:75 already exist in OtherFlags" -else - printfn "Directory.Build.props does not exist, creating it..." - let newContent = sprintf "\n \n \n $(OtherFlags) --nowarn:75 --times\n \n\n" absolutePropsPath - File.WriteAllText(propsFilePath, newContent) - printfn "✓ Created Directory.Build.props with UseLocalCompiler import and --times flag" - -printfn "" -printfn "Final Directory.Build.props content:" -printfn "-----------------------------------" -let content = File.ReadAllText(propsFilePath) -printfn "%s" content -printfn "-----------------------------------" -printfn "✓ Repository prepared for regression testing" diff --git a/eng/templates/regression-test-jobs.yml b/eng/templates/regression-test-jobs.yml index ba7a3c19dab..829debeb3f4 100644 --- a/eng/templates/regression-test-jobs.yml +++ b/eng/templates/regression-test-jobs.yml @@ -65,41 +65,20 @@ jobs: Write-Host "Successfully checked out ${{ item.repo }} at commit ${{ item.commit }}" git log -1 --oneline - + Write-Host "Repository structure:" Get-ChildItem -Name - - $buildScript = '${{ item.buildScript }}' - # Support ';;' separator for multiple commands — validate each command's script file - $commands = $buildScript -split ';;' | ForEach-Object { $_.Trim() } | Where-Object { $_ } - foreach ($cmd in $commands) { - if ($cmd -like "dotnet*") { - Write-Host "Built-in dotnet command, skipping file check: $cmd" - } else { - $scriptFile = ($cmd -split ' ', 2)[0] - Write-Host "Verifying build script exists: $scriptFile" - if (Test-Path $scriptFile) { - Write-Host "Build script found: $scriptFile" - } else { - Write-Host "Build script not found: $scriptFile" - Write-Host "Available files in root:" - Get-ChildItem - exit 1 - } - } - } displayName: Checkout ${{ item.displayName }} at specific commit - pwsh: | Set-Location $(Pipeline.Workspace)/TestRepo - Write-Host "Removing global.json to use latest SDK..." - if (Test-Path "global.json") { - Remove-Item "global.json" -Force - Write-Host "global.json removed" - } else { - Write-Host "No global.json found" - } - displayName: Remove global.json to use latest SDK + # Pin the test repo to the exact SDK that built the compiler (allowPrerelease + rollForward:disable) so its + # F# SDK targets and the runtime that runs the local fsc.dll line up, with no silent fallback. + $sdk = (Get-Content "$(Build.SourcesDirectory)/global.json" | ConvertFrom-Json).sdk.version + @{ sdk = @{ version = $sdk; allowPrerelease = $true; rollForward = "disable" } } | ConvertTo-Json | Set-Content "global.json" + Write-Host "Pinned test repo to SDK $sdk" + Get-Content "global.json" + displayName: Pin global.json to compiler SDK for ${{ item.displayName }} - task: UseDotNet@2 displayName: Install .NET SDK 8.0.x for ${{ item.displayName }} @@ -145,7 +124,7 @@ jobs: # into the regression test's .dotnet so fsc.dll can find the runtime. # Tries default feed first, then ci.dot.net/public (same fallback as eng/common). - pwsh: | - $v = (Get-Content "$(Build.SourcesDirectory)/global.json" | ConvertFrom-Json).tools.dotnet + $v = (Get-Content "$(Build.SourcesDirectory)/global.json" | ConvertFrom-Json).sdk.version $d = "$(Pipeline.Workspace)/TestRepo/.dotnet" $u = "https://builds.dotnet.microsoft.com/dotnet/scripts/v1" if ($IsWindows) { @@ -161,19 +140,6 @@ jobs: bash "$d/dotnet-install.sh" --version $v --install-dir $d --skip-non-versioned-files --azure-feed "https://ci.dot.net/public" } displayName: Install compiler SDK for ${{ item.displayName }} - continueOnError: true - - - pwsh: | - Set-Location $(Pipeline.Workspace)/TestRepo - - Write-Host "Running PrepareRepoForRegressionTesting.fsx..." - dotnet fsi $(Build.SourcesDirectory)/eng/scripts/PrepareRepoForRegressionTesting.fsx "$(Pipeline.Workspace)/Props/UseLocalCompiler.Directory.Build.props" - - if ($LASTEXITCODE -ne 0) { - Write-Host "Failed to prepare repository for regression testing" - exit 1 - } - displayName: Setup local compiler configuration for ${{ item.displayName }} - pwsh: | Set-Location $(Pipeline.Workspace)/TestRepo @@ -187,17 +153,17 @@ jobs: Write-Host "" Write-Host "F# Compiler artifacts available:" $productTfm = (dotnet msbuild "$(Pipeline.Workspace)/Props/TargetFrameworks.props" --getProperty:FSharpNetCoreProductTargetFramework).Trim() - Get-ChildItem "$(Pipeline.Workspace)/FSharpCompiler/bin/fsc/Release/$productTfm" -Name -ErrorAction SilentlyContinue + Get-ChildItem "$(Pipeline.Workspace)/FSharpCompiler/artifacts/bin/fsc/Release/$productTfm" -Name -ErrorAction SilentlyContinue Write-Host "" Write-Host "F# Core available:" - if (Test-Path "$(Pipeline.Workspace)/FSharpCompiler/bin/FSharp.Core/Release/netstandard2.0/FSharp.Core.dll") { + if (Test-Path "$(Pipeline.Workspace)/FSharpCompiler/artifacts/bin/FSharp.Core/Release/netstandard2.0/FSharp.Core.dll") { Write-Host "FSharp.Core.dll found" } else { Write-Host "FSharp.Core.dll not found" } Write-Host "" - Write-Host "Directory.Build.props content:" - Get-Content "Directory.Build.props" + Write-Host "Directory.Build.props content (none if injected via CustomAfterDirectoryBuildProps):" + Get-Content "Directory.Build.props" -ErrorAction SilentlyContinue Write-Host "" Write-Host "===========================================" displayName: Report build environment for ${{ item.displayName }} @@ -216,14 +182,6 @@ jobs: - pwsh: | Set-Location $(Pipeline.Workspace)/TestRepo - Write-Host "============================================" - Write-Host "Starting build for ${{ item.displayName }}" - Write-Host "Repository: ${{ item.repo }}" - Write-Host "Commit: ${{ item.commit }}" - Write-Host "Build Script: ${{ item.buildScript }}" - Write-Host "============================================" - Write-Host "" - $errorLogPath = "$(Pipeline.Workspace)/build-errors.log" $fullLogPath = "$(Pipeline.Workspace)/build-full.log" @@ -242,62 +200,24 @@ jobs: } } - function Run-Command { - param([string]$cmd) - if ($cmd -like "dotnet*") { - Write-Host "Executing built-in command: $cmd" - if ($IsWindows) { - cmd /c $cmd 2>&1 | Tee-Object -FilePath $fullLogPath -Append | ForEach-Object { - Process-BuildOutput $_ - } - } else { - # Escape semicolons for bash -c to prevent them being treated as command separators - $escapedCmd = $cmd -replace ';', '\;' - bash -c "$escapedCmd" 2>&1 | Tee-Object -FilePath $fullLogPath -Append | ForEach-Object { - Process-BuildOutput $_ - } - } - } elseif ($IsWindows) { - Write-Host "Executing file-based script: $cmd" - cmd /c ".\$cmd" 2>&1 | Tee-Object -FilePath $fullLogPath -Append | ForEach-Object { - Process-BuildOutput $_ - } - } else { - Write-Host "Executing file-based script: $cmd" - $scriptFile = ($cmd -split ' ', 2)[0] - chmod +x "$scriptFile" - bash -c "./$cmd" 2>&1 | Tee-Object -FilePath $fullLogPath -Append | ForEach-Object { - Process-BuildOutput $_ - } - } - return $LASTEXITCODE - } + # Build logic lives in the fsx so a failure reproduces with the same one command, no pipeline required. + $fsxArgs = @( + "$(Build.SourcesDirectory)/eng/scripts/BuildWithLocalFSharp.fsx", + '--compiler-path', "$(Pipeline.Workspace)/FSharpCompiler", + '--props', "$(Pipeline.Workspace)/Props/UseLocalCompiler.Directory.Build.props", + '--core-packages-dir', "$(Pipeline.Workspace)/Props/library-packs", + '--nuget-packages', "$(Pipeline.Workspace)/.nuget-packages", + '--build-script', '${{ item.buildScript }}' + ) + if ('${{ item.expectLocalCore }}' -eq 'True') { $fsxArgs += '--verify' } - # Support ';;' separator for multiple commands - $commands = ('${{ item.buildScript }}' -split ';;') | ForEach-Object { $_.Trim() } | Where-Object { $_ } - - foreach ($cmd in $commands) { - $exitCode = Run-Command $cmd - if ($exitCode -ne 0) { - Write-Host "" - Write-Host "============================================" - Write-Host "Command failed: $cmd" - Write-Host "Exit code: $exitCode" - Write-Host "============================================" - exit $exitCode - } + dotnet fsi @fsxArgs 2>&1 | Tee-Object -FilePath $fullLogPath -Append | ForEach-Object { Process-BuildOutput $_ } + $code = $LASTEXITCODE + if ($code -ne 0) { + Write-Host "##[error]Build failed for ${{ item.displayName }} (exit code $code)" + exit $code } - - Write-Host "" - Write-Host "============================================" - Write-Host "Build completed for ${{ item.displayName }}" - Write-Host "Exit code: 0" - Write-Host "============================================" displayName: Build ${{ item.displayName }} with local F# compiler - env: - LocalFSharpCompilerPath: $(Pipeline.Workspace)/FSharpCompiler - LoadLocalFSharpBuild: 'True' - LocalFSharpCompilerConfiguration: Release timeoutInMinutes: 120 - pwsh: | @@ -417,27 +337,14 @@ jobs: } Write-Host "" - Write-Host "##[section]LOCAL REPRODUCTION STEPS (from fsharp repo root):" - Write-Host "# 1. Build the F# compiler" - Write-Host "./build.sh -c Release" - Write-Host "" - Write-Host "# 2. Clone and checkout the failing library" - Write-Host "cd .." + $verifyFlag = if ('${{ item.expectLocalCore }}' -eq 'True') { ' --verify' } else { '' } + Write-Host "##[section]LOCAL REPRODUCTION (any OS; FSHARP_REPO = your dotnet/fsharp checkout built with build.sh/Build.cmd -c Release -pack):" Write-Host "git clone --recursive https://github.com/${{ item.repo }}.git TestRepo" - Write-Host "cd TestRepo" - Write-Host "git checkout ${{ item.commit }}" - Write-Host "git submodule update --init --recursive" - Write-Host "rm -f global.json" - Write-Host "" - Write-Host "# 3. Prepare the repo for local compiler" - Write-Host "dotnet fsi ../fsharp/eng/scripts/PrepareRepoForRegressionTesting.fsx `"../fsharp/UseLocalCompiler.Directory.Build.props`"" - Write-Host "" - Write-Host "# 4. Build with local compiler" - Write-Host "export LocalFSharpCompilerPath=`$PWD/../fsharp" - Write-Host "export LoadLocalFSharpBuild=True" - Write-Host "export LocalFSharpCompilerConfiguration=Release" - Write-Host "${{ item.buildScript }}" - + Write-Host "cd TestRepo; git checkout ${{ item.commit }}; git submodule update --init --recursive" + Write-Host 'BUILD_COMMAND: ${{ item.buildScript }}' + Write-Host "dotnet fsi FSHARP_REPO/eng/scripts/BuildWithLocalFSharp.fsx$verifyFlag --build-script ''" + Write-Host "# If TestRepo/global.json pins a different SDK, align sdk.version to your compiler SDK (rollForward: disable); add --nuget-packages to isolate restore." + Write-Host "##vso[task.logissue type=error;sourcepath=azure-pipelines-PR.yml]Regression test failed: ${{ item.displayName }}" } Write-Host "============================================" diff --git a/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/SolutionExplorerInProcess.cs b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/SolutionExplorerInProcess.cs index 43743484e96..7b330eddc6f 100644 --- a/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/SolutionExplorerInProcess.cs +++ b/vsintegration/tests/FSharp.Editor.IntegrationTests/InProcess/SolutionExplorerInProcess.cs @@ -60,6 +60,7 @@ private static string CreateStandaloneProjectFile() return $@" + True Debug {RepoRoot}