This document describes how multiple .NET SDK bands are managed in the Unified Build world using the VMR (Virtual Monolithic Repository). This approach allows us to maintain multiple SDK versions that share common components while keeping band-specific components separate.
This section presents more precise definitions of common terms used in this document that may be prone to confusion. Also see the Unified Build terminology for more.
- Individual/Source repository – One of the current development repositories, e.g.,
dotnet/runtime. An "individual product repository" is then one that contains code for part of the product (but no individual repository contains code to build the whole .NET Core product). - VMR (Virtual Monolithic Repository) – A repository containing code layout that produces the official build product of .NET Core. The repository contains individual product repositories plus tooling to enable a full build of the product.
- VMR-lite – present state where changes only flow one way into the VMR which is read-only.
- Full VMR (Backflow) – future final state where changes can be made in the VMR too and flow both ways. Dependency flow at that time will be only between VMR and individual repos, not between individual repos themselves.
- Source-Build – A set of sources and a process which allows to build the entire product end to end including all its dependencies in offline mode, excluding native dependencies from the source.
- Microsoft build – The current build methodology used to assemble the final product that Microsoft ships binaries from.
- SDK branch – A git branch related to a specific SDK band, e.g.
release/8.0.1xx. - Non-SDK branch – A git branch common for all associated SDK bands, e.g.
release/8.0. - Build output packages – Packaged build products of each of the individual repositories either built in their individual repo source-build or during the build of each individual repository component within the full VMR build. These are used during package flow between the VMR and the individual repositories, and in the VMR build itself.
- Shared component - A component that is shared between multiple SDK bands. For example, the .NET runtime is shared between all SDK bands.
- Band-specific component - The opposite of a shared component. A component whose version differs between SDK bands.
- Maestro - a service used by the .NET team to manage dependency flow between repositories.
To align with new Visual Studio releases, .NET SDK updates sometimes include new features or new versions of components such as Roslyn or MSBuild. These new features or components may be incompatible with the versions that shipped in previous SDK updates for the same major or minor version. To differentiate such updates, the .NET SDK uses the concept of feature bands. While these bands differ in their feature set they share some common parts such as the .NET runtime.
The VMR uses SDK branches to manage different bands, similar to how individual repositories work today. Each SDK branch contains the appropriate sources for that band:
# release/9.0.1xx branch (100th band)
└── src
├── arcade
├── roslyn
├── runtime
└── sdk
# release/9.0.2xx and other branches (non-100th bands)
└── src
├── roslyn # references the runtime and arcade build output packages instead of sources
└── sdk # references the runtime and arcade build output packages instead of sourcesKey characteristics:
- VMR has SDK branches, e.g.
release/9.0.1xxandrelease/9.0.2xx - Each repository is a folder under
src/in the 1xx branch of the VMR - Each non-1xx branch contains only band-specific repositories (e.g., roslyn, sdk)
- Shared components (runtime, arcade) exist as sources only in the 1xx branch
- Non-1xx branches reference shared components as build output packages from the 1xx branch
A shared component is a component that is shared between multiple SDK bands. For instance, the .NET runtime is a good example of a shared component while the Roslyn compiler would typically differ between bands.
During the development cycle, it can happen that shared components require band-specific changes and they can become band-specific for some time. Usually, this is a point in time event and the component becomes shared again after some time. A good example of this is the Arcade repository which contains build tools/infrastructure. However, there are no strict rules about this and it is possible that a component remains band-specific.
To best illustrate how SDK bands are developed and released in practice, let’s imagine the following timeline for repositories with SDK branches (e.g., dotnet/sdk):
%%{init: { 'gitGraph': {'showCommitLabel': false }}}%%
gitGraph
commit
commit
branch release/8.0.1xx
checkout release/8.0.1xx
commit
commit type: HIGHLIGHT tag: "Release #1 – SDK 8.0.100"
commit
checkout main
commit
commit
commit
checkout release/8.0.1xx
branch release/8.0.2xx
checkout release/8.0.2xx
commit
commit
commit type: HIGHLIGHT tag: "Release #2 – SDK 8.0.205"
checkout main
commit
checkout release/8.0.1xx
commit type: HIGHLIGHT tag: "Release #2 – SDK 8.0.109"
commit
commit
checkout main
commit
checkout release/8.0.1xx
commit
commit
checkout release/8.0.2xx
commit
checkout release/8.0.1xx
commit
checkout main
commit
commit
commit
checkout release/8.0.2xx
branch release/8.0.3xx
checkout release/8.0.3xx
commit
commit
checkout release/8.0.2xx
commit type: HIGHLIGHT tag: "Release #3 – SDK 8.0.207"
checkout release/8.0.1xx
commit type: HIGHLIGHT tag: "Release #3 – SDK 8.0.111"
checkout release/8.0.3xx
commit type: HIGHLIGHT tag: "Release #3 – SDK 8.0.302"
checkout main
commit
In parallel, this would represent the state of a shared repository (e.g., dotnet/runtime) that is not specific to any particular SDK band:
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'showCommitLabel': false, 'tagLabelFontSize': '24px'}} }%%
gitGraph
commit
commit
branch release/8.0
checkout release/8.0
commit
commit type: HIGHLIGHT tag: "Release #1 – Runtime 8.0.0"
commit
checkout main
commit
commit
commit
checkout release/8.0
commit
commit
commit type: HIGHLIGHT tag: "Release #2 – Runtime 8.0.1"
checkout main
commit
commit
commit
checkout release/8.0
commit
commit
checkout main
commit
checkout release/8.0
commit
commit
checkout main
commit
commit
commit
checkout release/8.0
commit
commit type: HIGHLIGHT tag: "Release #3 – Runtime 8.0.2"
checkout main
commit
commit
On the image you can see timelines of branches of two different repositories – dotnet/sdk and dotnet/runtime. As noted previously, each (servicing) release of .NET may contain multiple SDK bands but only one runtime. Each individual repository of each component that needs to differ per band would then have these so-called “SDK branches” named release/Z.0.Yxx while repositories that are shared per release have the non-SDK release/Z.0 branches. As shown in the example, the development of the single runtime would happen in the release/8.0 branch while the various SDK bands are stored in the following SDK branches (e.g., release/8.0.1xx represents the “100th band”).
Once we hit each release day (denoted with red vertical lines), we take the latest commit of each of those branches (that already exist) and release those together. For the releases in the example diagram, these are the released compilations:
| Release | Runtime | SDKs |
|---|---|---|
| #1 | 8.0.0 |
8.0.100 |
| #2 | 8.0.1 |
8.0.109, 8.0.205 |
| #3 | 8.0.2 |
8.0.111, 8.0.207, 8.0.302 |
To organize what ends up in each band and to drive the code flow between the repositories, we utilize the Maestro dependency flow, namely the Maestro channels (see Channels, Branches and Subscriptions for details):
- VS-centric channels – To better match how teams operate, some repositories align their build outputs with the Visual Studio versions, e.g.
dotnet/roslyn. Outputs of repositories like that would end up in a channel named based on the version of VS, e.g.17.5. - SDK band channels – The repositories that are closer to how we organize the final release are then targeting channels named based on the band version, e.g.
.NET 7.0.3xx SDK. - Shared component channels – Lastly, repositories with shared components and tooling repositories target channels named based on the major .NET version, e.g.
.NET 7or.NET 7 Eng.
The following diagram shows a simplified example (some relationships are left out for brevity such as tooling leading to all repositories):
flowchart TD
classDef Channel fill:#2487DF,stroke:#fff,stroke-width:1px,color:#fff;
roslyn174[dotnet/roslyn<br />dev/17.4]
channel174[(VS 17.4 channel)]
sdk2xx[dotnet/sdk<br />release/7.0.2xx]
channel2xx[(.NET 7.0.2xx SDK<br />channel)]
subgraph Shared components
runtime[dotnet/runtime<br />release/7.0]
aspnetcore[dotnet/aspnetcore<br />release/7.0]
arcade[dotnet/arcade<br />release/7.0]
channel7[(.NET 7<br />channel)]
channel7Eng[(.NET 7 Eng<br />channel)]
end
roslyn175[dotnet/roslyn<br />dev/17.5]
channel175[(VS 17.5 channel)]
sdk3xx[dotnet/sdk<br />release/7.0.3xx]
channel3xx[(.NET 7.0.3xx SDK<br />channel)]
roslyn174-->channel174
channel174-->sdk2xx
sdk2xx-->channel2xx
channel2xx-->installer2xx
runtime-->channel7
aspnetcore-->channel7
arcade-->channel7Eng
channel7-->installer3xx
channel7-->installer2xx
channel7Eng-->installer2xx
channel7Eng-->installer3xx
roslyn175-->channel175
channel175-->sdk3xx
sdk3xx-->channel3xx
channel3xx-->installer3xx
installer2xx[dotnet/installer<br />release/7.0.2xx]
installer3xx[dotnet/installer<br />release/7.0.3xx]
class channel174,channel175,channel2xx,channel3xx,channel7,channel7Eng Channel
This setup makes sure that the latest version of each shared component (e.g., runtime) eventually flows to all SDK products. Over time, the SDK products become coherent. We call the SDK bands coherent when the versions of all shared components of each band are the same.
The band lifecycle is tightly coupled with the releases of Visual Studio. The exception is the 100th band that ships on .NET's annual schedule and VS snaps to it. For repositories that target the VS-centric bands, the main branch usually targets the next VS version which is in preview. Once a version of VS is released (is GA-ed), we create a new branch named dev/XX.Y where XX.Y is the version of the released VS. The main branch then targets the next VS version in preview.
-
Product preview time: The preview time is when most development happens and the VMR contains a single band only (the 1xx branch). Everything works the same way as with a single version.
-
Band preview time: The band that is created latest and is to be released next is called the preview band. Except for the 1xx band, each preview band is locked down to use the latest released version of the shared components during development. Since non-1xx branches don't contain sources of shared components, it's clear that dependencies come from packages.
-
Band servicing: Once a band is released, it moves into servicing mode where it receives updates from the shared component channels (like
.NET 9.0channel) for runtime updates.
To create a new band, the snap is performed in the VMR and then flowed to individual repositories:
-
Create the new branch based off the current preview band
- E.g., branch
release/9.0.3xxfromrelease/9.0.2xx
- E.g., branch
-
Remove shared component sources in the new branch
- Remove sources of shared components (runtime, arcade, etc.)
- Adjust package versions to point to build output packages from the last release
-
Configure Maestro subscriptions between new VMR bands and their individual repository counterparts
-
Configure package flow (when there are at least 3 bands)
- Configure subscriptions for the currently released band to consume build output packages from the 1xx band
-
Flow changes to individual repositories
- Maestro flows the changes from VMR and creates appropriate branches in individual repositories
This ensures that the new (preview) band is locked to use the latest released shared components and that newly released bands will start getting the newest shared components built in the 1xx branch.
As an example, let's say we have the following setup:
- VS
17.1is the latest stable version of VS and is associated with the9.0.1xxband - VS
17.2is in preview and is associated with the9.0.2xxband - VS-centric repositories would have a
dev/17.1branch targeting the17.1channel and amainbranch targeting17.2 - The
17.1channel would then flow into the9.0.1xxbranches and the17.2channel would flow into the9.0.2xx - The
9.0.1xxbranches would then flow into the9.0.1xxSDK channel and the9.0.2xxbranches would flow into the9.0.2xxSDK channel - The
9.0channels would collect builds of shared repositories and flow into9.0.Yxxbranches that are currently in servicing - The
9.0 Engchannels would collect builds of shared eng repositories and flow into their respective all9.0.Yxxbranches, including previews
For this setup, we'd say the 100th band is in servicing and the 200th band is in preview. It is important to note that while a band is in preview, it uses the most recently released .NET runtime while the servicing band revs with the 9.0 channel.
When we would be ready to release VS 17.2, we'd flow the latest shared components into the 200th band branches so that it becomes coherent with the 100th band. Then we'd release the coherent bands and after we would do the following:
- Create the
dev/17.2branch in VS-centric repositories, point it to the17.2channel and retargetmaintodev/17.3 - Snap branches of SDK repositories by branching
9.0.3xxfrom9.0.2xx. While doing that, we'd update the runtime version of9.0.3xxto the just released version - Set up the
9.0shared channels to start flowing into the9.0.2xxbranches as the runtime there would start revving
The important part is the alignment of repository's development branches with the Maestro channels.
With the full VMR backflow model, individual repositories only receive and send updates from/to the VMR and not between each other, so the situation looks like this:
flowchart TD
VMR[VMR]
arcade[dotnet/arcade]
runtime[dotnet/runtime]
roslyn[dotnet/roslyn]
sdk[dotnet/sdk]
other[...]
arcade-->VMR
runtime-->VMR
roslyn-->VMR
sdk-->VMR
other-->VMR
VMR-->arcade
VMR-->runtime
VMR-->roslyn
VMR-->sdk
VMR-->other
A regular forward flow with changes going to the VMR only would look like this:
sequenceDiagram
autonumber
participant runtime as dotnet/runtime<br />release/9.0
participant SDK_1xx as dotnet/sdk<br />release/9.0.1xx
participant SDK_2xx as dotnet/sdk<br />release/9.0.2xx
participant VMR_1xx as VMR<br />release/9.0.1xx
participant VMR_2xx as VMR<br />release/9.0.2xx
runtime->>runtime: New change ➡️ RUN_2
runtime->>VMR_1xx: Flow of 📄 RUN_2
Note over VMR_1xx: 📦 VMR_2 build output packages are built
VMR_1xx->>VMR_2xx: Flow of 📦 VMR_2<br />(runtime build output packages)
Note over VMR_2xx: 📦 VMR_3 build output packages are built
Note over VMR_2xx: ✅ Coherent state<br />VMR 1xx and 2xx have 📄 RUN_2
par Parallel backflow of build output packages
VMR_1xx->>SDK_1xx: Backflow of 📦 VMR_2
and
VMR_2xx->>SDK_2xx: Backflow of 📦 VMR_3
end
For breaking changes, the situation becomes more complex. Let's imagine a situation where a change is needed in one of the bands that requires a breaking change in a shared component:
sequenceDiagram
autonumber
participant runtime as dotnet/runtime<br />release/9.0
participant SDK_1xx as dotnet/sdk<br />release/9.0.1xx
participant SDK_2xx as dotnet/sdk<br />release/9.0.2xx
participant VMR_1xx as VMR<br />release/9.0.1xx
participant VMR_2xx as VMR<br />release/9.0.2xx
runtime->>runtime: Change in runtime ➡️ RUN_2
runtime->>VMR_1xx: PR with source change to 📄 RUN_2 is opened
activate VMR_1xx
Note over VMR_1xx: ❌ Requires a change in SDK
VMR_1xx->>VMR_1xx: Change needed in src/sdk<br />Creating 📄 SDK_1.2
deactivate VMR_1xx
Note over VMR_1xx: 📦 VMR_2 build output packages are built
VMR_1xx->>SDK_1xx: Flow of 📄 SDK_1.2, 📦 VMR_2
VMR_1xx->>VMR_2xx: Flow of 📦 VMR_2
activate VMR_2xx
Note over VMR_2xx: ❌ Requires a change in SDK
VMR_2xx->>VMR_2xx: Change needed in src/sdk<br />Creating 📄 SDK_2.2
deactivate VMR_2xx
Note over VMR_2xx: 📦 VMR_3 build output packages are built
VMR_2xx->>SDK_2xx: Flow of 📄 SDK_2.2, 📦 VMR_3
Note over VMR_2xx: ✅ Coherent state<br />VMR 1xx and 2xx both use 📄 RUN_2
This flow ensures that after breaking changes, all SDK bands become coherent by using the same version of shared components.
The proposed layout has some problematic implications. Let's consider the following scenarios:
- A developer needs to make sure a cross-repo change to
src/runtimeandsrc/sdkin the3xxband. - A distro maintainer needs to build the
3xxband from source.
We need to make sure both of these scenarios are easy to do but the layout of the sources doesn't allow that out of the box.
It seems that to make this work, we'd need to be able to tell Source Build to easily swap between using the sources and the build output packages of the shared components. When someone would be interested in these flows, we should have a mechanism to also checkout the sources and reference them during the build. There are couple of possibilities:
- The
src/folder of non-1xx branches would contain submodules pointing to the original individual repositories. These would not be used in most flows but could be activated. When we'd be flowing changes from the 1xx branch, we could also change where the submodule points. - Have a script that would check the components out into
src/onto the same location where they are placed in the 1xx branch. It would also create some dummy file to signal that Source Build should ignoreVersion.Details.xmlwhen restoring the build output packages but rather build the sources. Thesrc/locations and the signal file would be ".gitignored". The dev would then have to backport their changes from withing the folders to either the 1xx branch or the individual repositories. - We could just expect the individual repositories to be checked out somewhere else on developer's disk (e.g. next to the VMR itself) and Source Build would know to find and build those instead (again through an invisible signal file for instance).
The first option seems quite straightforward but the individual repository doesn't necessarily have to have the same contents as its counterpart in the VMR which might be problematic.
The second flow solves the scenario of .NET distro maintainers fully as we'd easily create a source tarball that would match the layout of the 1xx branch. It doesn't work well for the developer scenario though.
The third option seems to be the best for the developer but doesn't solve the distro maintainer scenario fully.
It seems that we could have a mixture of 2. and 3. where Source Build would have a feature of "know to look elsewhere" and distro maintainers would build from a tarball assembled as described in 2. and developers would have the option to use the sources from their local individual repositories as shown in 3..
For distro maintainers who need to build from source:
- 1xx band builds: Contain all necessary sources and can be built directly
- Non-1xx band builds: Require assembling sources from the 1xx branch for shared components or using the mechanisms described above to swap from packages to sources
The release process has three main phases:
We need to ensure SDK branches are coherent, meaning all bands use the same versions of shared components. This requires:
- The latest published build output packages from the 1xx branch have flowed to all other SDK band branches
- Enabling package flow for the preview band to consume the newest bits
- Validating everything works together
Since shared components are built only once and stored in build output packages, we can assemble packages from all band branches and release them together, similar to how we do it today. The staging pipeline assembles build products from official builds of each band.
Publishing sources for consumption by 3rd party partners differs based on consumer needs:
-
Single SDK band release: Only the 1xx band branch contains sources that can be built directly. Non-1xx band branches don't contain source code of shared components and only reference them as build output packages. We need to compile sources by restoring them from the 1xx band branch.
-
Multiple SDK release: We need to compile the full set of sources by bringing the branches together, ensuring shared components appear only once.
The release process ensures that shared components are built once officially and reused across all band builds, maintaining coherency while optimizing build times.