Skip to content

Allow passing a project to build via stdin#13301

Draft
Copilot wants to merge 7 commits intomainfrom
copilot/allow-passing-project-via-stdin
Draft

Allow passing a project to build via stdin#13301
Copilot wants to merge 7 commits intomainfrom
copilot/allow-passing-project-via-stdin

Conversation

Copy link
Contributor

Copilot AI commented Feb 26, 2026

Enables piping MSBuild project content directly via stdin, eliminating the need for a physical project file on disk:

echo "<Project><Target Name='Build'><Message Text='hi'/></Target></Project>" | dotnet build

Context

Users should be able to create a text model of a project, pipe it to stdin, and have MSBuild build it directly — without requiring a project file on disk.

Changes Made

src/MSBuild/XMake.cs

  • CanRunServerBasedOnCommandLineSwitches: returns false when Console.IsInputRedirected — the MSBuild server process cannot access the client's stdin
  • ProcessCommandLineSwitches: when no project is explicitly specified and stdin is redirected, reads all stdin content; if non-empty, stores it in s_stdinProjectContent and sets projectFile to a synthetic display path (stdin.proj in the current working directory) — no file is written to disk
  • BuildProject: after the ProjectCollection is created, parses the stored stdin XML into an in-memory Project via LoadStdinProject, which registers the ProjectRootElement in the ProjectRootElementCache under the synthetic path. All subsequent code paths (preprocess, targets listing, main build, restore) call LoadProject(projectFile, …) or create BuildRequestData(projectFile, …), producing a cache hit that returns the in-memory project without any disk I/O.
  • BuildProject: sets NodeAffinity.InProc via parameters.HostServices for the stdin project, ensuring it always builds on the central in-proc node — worker nodes run in separate processes and cannot access the in-memory ProjectRootElement.
  • BuildProject (restore path): after ExecuteRestore calls ClearCachesAfterBuild (which wipes the entire ProjectRootElementCache), the code unloads the old Project and calls LoadStdinProject again to re-register the project in the now-empty cache before the subsequent default build starts.
  • Execute evaluation path: the --getProperty/--getItem code path creates its own ProjectCollection, so it re-parses the content from s_stdinProjectContent via LoadStdinProject directly.
  • Execute finally block: clears s_stdinProjectContent = null — no temp file to delete
  • Added s_stdinProjectContent static field to store the piped XML content across calls
  • Added LoadStdinProject helper method to avoid duplication of the XML-parse + FullPath-assignment logic

src/MSBuild.UnitTests/XMake_Tests.cs

  • BuildProjectPipedViaStdin: end-to-end test spawning MSBuild as a subprocess with project XML piped to stdin, asserting successful build and expected output
  • BuildProjectPipedViaStdin_WithRestore: end-to-end test for the -restore + default build path, asserting both the Restore and Build targets ran — exercises the cache re-registration fix
  • BuildProjectPipedViaStdin_SpecificTarget: end-to-end test for an explicit -t:Target invocation
  • RunMSBuildWithStdinProject: shared helper that spawns the MSBuild subprocess and pipes project XML to stdin, used by all three tests

Testing

Three E2E tests spawn MSBuild as a subprocess with project XML piped to stdin:

  • Basic build: verifies the project is loaded and the target output is produced
  • Restore + build: verifies that -restore runs the Restore target and the subsequent default build still finds the in-memory project after the cache is cleared
  • Specific target: verifies -t:Target works with piped projects

Existing ProcessProjectSwitch tests and response-file implicit discovery tests continue to pass unmodified — stdin is only read when projectParameters.Length == 0 && Console.IsInputRedirected and the content is non-empty.

Notes

  • Stdin is only consumed when all of the following hold: stdin is redirected (piped), no project was explicitly provided on the command line or auto-response file, and stdin is non-empty. Empty//dev/null stdin falls through to the normal "find a project in the current directory" behavior.
  • No temp file is written. The piped XML is held entirely in memory as a ProjectRootElement registered in the ProjectRootElementCache under the synthetic path stdin.proj in the current working directory. Relative paths inside the piped project content resolve relative to the current working directory, matching expected behavior.
  • MSBuild server mode is disabled when stdin is redirected, since the server process cannot access the client's stdin.
  • Virtual (in-memory) stdin projects are pinned to the in-proc node via NodeAffinity.InProc. After a -restore clears the cache, the project is automatically re-registered before the main build begins.
Original prompt

This section details on the original issue you should resolve

<issue_title>Allow for passing a project-to-build via contents over stdin</issue_title>
<issue_description>### Summary

Users should be able to create a text model of a project, then pipe it to stdin and have MSBuild use that input stream to create a Project(Instance) for the build request.

Background and Motivation

This would make it easier to simulate multi-project builds and might reduce the need for us to do more extreme work.

Proposed Feature

echo "<Project></Project>" | dotnet build should work.

Alternative Designs

No response</issue_description>

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

@dotnet-policy-service
Copy link
Contributor

Hello @@copilot, I noticed that you’re changing an .swr file or any file under src/Package/MSBuild.VSSetup.. Please make sure to validate this change by an experimental VS insertion. This is accomplished by pushing to an exp/* branch, which requires write permissions to this repo.

Copilot AI changed the title [WIP] Allow passing a project-to-build via contents over stdin Allow passing a project to build via stdin Feb 26, 2026
@baronfel
Copy link
Member

@copilot is it possible to not emit the temp project at all and use purely in-memory virtual projects?

Copy link
Contributor Author

Copilot AI commented Feb 27, 2026

@copilot is it possible to not emit the temp project at all and use purely in-memory virtual projects?

Yes! Implemented in commit 851424b. The approach:

  1. The stdin XML is parsed into a ProjectRootElement using ProjectRootElement.Create(XmlReader, projectCollection)_directory is automatically set to the current working directory.
  2. Setting xml.FullPath = "stdin.proj" (a synthetic display path in the cwd, never written to disk) registers the element in the ProjectRootElementCache.
  3. All existing code paths — preprocess, targets listing, main build, restore — call LoadProject(projectFile, …) or create BuildRequestData(projectFile, …), which trigger a cache hit and return the in-memory project without any disk I/O.
  4. A local variable stdinXml (with GC.KeepAlive) keeps the cache entry from being evicted while the build runs.
  5. The separate --getProperty/--getItem evaluation path in Execute() uses a different ProjectCollection, so it loads fresh from the stored XML string via XmlReader.

No temp file is created or deleted anywhere.

@baronfel
Copy link
Member

@copilot try to add some tests for this feature now - for project builds that do a -restore the subsequent default build fails, I believe because it is schedule on a node other than the central node. We need to make sure that all builds of virtual projects (ones with IsExplicitlyLoaded=true) happen on the node they were loaded on (which should be the central node)

Copilot AI and others added 2 commits February 27, 2026 20:47
Copy link
Contributor Author

Copilot AI commented Feb 27, 2026

@copilot try to add some tests for this feature now - for project builds that do a -restore the subsequent default build fails, I believe because it is schedule on a node other tha...

Added tests and fixed the -restore + build failure in commit 73959e8. Two fixes:

  1. NodeAffinity.InProc — sets parameters.HostServices.SetNodeAffinity(projectFile, NodeAffinity.InProc) so all builds of the virtual stdin project are scheduled on the central in-proc node. Worker nodes run in separate processes and can't see the in-memory ProjectRootElement.

  2. Cache re-registration after restoreExecuteRestore uses ClearCachesAfterBuild which calls ProjectRootElementCache.Clear(), wiping all entries including the in-memory project. After restore succeeds (and before the main build starts), we now call projectCollection.UnloadProject(stdinProject) then LoadStdinProject(projectCollection, projectFile) to re-register the project in the now-empty cache.

Three tests added:

  • BuildProjectPipedViaStdin (existing, refactored)
  • BuildProjectPipedViaStdin_WithRestore — restore + build via -restore, verifies both Restore and Build targets ran
  • BuildProjectPipedViaStdin_SpecificTarget — explicit -t:Target

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow for passing a project-to-build via contents over stdin

2 participants