microsoft / microsoft/WindowsAppSDK

[Bootstrap/Deployment] Unpackaged WinAppSDK 2.x apps resolve the latest compatible runtime and cannot select the build-time minor version

Open
#6,755 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

needs-triage
Dominant language
C++
Stars
4.7k
Forks
471
Avg merge
3d 13h
Merged PRs (30d)
28

Description

## Describe the issue

For Windows App SDK 2.x, an **unpackaged, framework-dependent** application has no supported way to select the Windows App Runtime minor version against which it was built and tested.

In testing, this also produces a behavioral difference between packaged and unpackaged framework-dependent applications built against the same Windows App SDK version:

* the packaged application resolves the Windows App Runtime version associated with its package dependency as expected in the tested scenario;
* the unpackaged application resolves to the newest installed compatible Windows App Runtime 2.x version.

The important point is not simply that "a newer DLL gets loaded".

The difference comes from how the Windows App Runtime framework package is inserted into the process package graph.

For packaged applications, Windows creates a static package graph from the package manifest before process startup.

For unpackaged applications, Windows App SDK uses the Bootstrapper and Dynamic Dependency APIs to dynamically resolve and insert a framework package into the package graph.

For Windows App SDK 2.x, that Bootstrap path no longer carries enough version information to identify a particular minor release.

---

## Observed behavior

Assume an application is built against Windows App SDK 2.0.x and is framework-dependent.

When multiple Windows App Runtime 2.x versions are installed, for example:

```text
Microsoft.WindowsAppRuntime.2 2.0.x
Microsoft.WindowsAppRuntime.2 2.1.x
Microsoft.WindowsAppRuntime.2 2.4.x
```

the following behavior is observed:

```text
Packaged framework-dependent app built against 2.0.x
-> resolves the expected packaged/static framework dependency in the tested scenario

Unpackaged framework-dependent app built against 2.0.x
-> resolves the newest compatible Windows App Runtime 2.x package
```

For the unpackaged application, this happens in both Debug and Release configurations and is independent of Visual Studio debugging.

The executable itself does not need to be rebuilt for its effective Windows App Runtime version to change.

Installing a newer compatible Windows App Runtime 2.x and starting the same executable again can cause it to run against the newer runtime.

---

## Why the unpackaged application behaves this way

For an unpackaged, non-self-contained executable, Windows App SDK automatically enables Bootstrap initialization.

`Microsoft.WindowsAppSDK.BootstrapCommon.targets` contains:

```xml

true

```

The auto-initializer then calls:

```cpp
const UINT32 c_majorMinorVersion{ WINDOWSAPPSDK_RELEASE_MAJORMINOR };
PCWSTR c_versionTag{ WINDOWSAPPSDK_RELEASE_VERSION_TAG_W };
const PACKAGE_VERSION c_minVersion{ WINDOWSAPPSDK_RUNTIME_VERSION_UINT64 };

const HRESULT hr{
::MddBootstrapInitialize2(
c_majorMinorVersion,
c_versionTag,
c_minVersion,
c_options)
};
```

The important detail is that:

```cpp
WINDOWSAPPSDK_RUNTIME_VERSION_UINT64
```

is passed as:

```cpp
minVersion
```

not as an exact version.

The actual framework package is selected later in `FirstTimeInitialization()`:

```cpp
const std::wstring frameworkPackageFamilyName{
GetFrameworkPackageFamilyName(
majorMinorVersion,
packageVersionTag.c_str())
};

THROW_IF_FAILED(
MddCore::Win11::TryCreatePackageDependency(
nullptr,
frameworkPackageFamilyName.c_str(),
minVersion,
architectureFilter,
lifetimeKind,
nullptr,
createOptions,
&packageDependencyId));

THROW_IF_FAILED(
MddCore::Win11::AddPackageDependency(
packageDependencyId.get(),
MDD_PACKAGE_DEPENDENCY_RANK_DEFAULT,
addOptions,
&packageDependencyContext,
&packageFullName));
```

This means the Bootstrapper does not directly select a concrete Windows App Runtime package.

Instead, it creates a dependency using:

```text
package family
+
minimum version
```

and Windows resolves that dependency to a concrete `packageFullName`.

That resolved package is then inserted into the current process package graph.

---

## Windows App SDK 2.x intentionally ignores the minor version

This behavior appears to originate from the Windows App SDK 2.0 versioning redesign.

For Windows App SDK 1.x, `GetFrameworkPackageFamilyName()` includes both major and minor:

```cpp
frameworkPackageFamilyName = std::format(
L"{}.{}.{}{}{}_8wekyb3d8bbwe",
namePrefix,
majorVersion,
minorVersion,
packageVersionTagDelimiter,
packageVersionTag);
```

Conceptually:

```text
Microsoft.WindowsAppRuntime.1.7
Microsoft.WindowsAppRuntime.1.8
```

These are different package families.

Therefore, selecting a different `major.minor` value in the Bootstrap API selected a different framework package family.

For Windows App SDK 2.x, the implementation changes to:

```cpp
frameworkPackageFamilyName = std::format(
L"{}.{}{}{}_8wekyb3d8bbwe",
namePrefix,
majorVersion,
packageVersionTagDelimiter,
packageVersionTag);
```

The minor version is deliberately omitted.

Conceptually:

```text
Windows App SDK 2.0
Windows App SDK 2.1
Windows App SDK 2.4
```

all belong to:

```text
Microsoft.WindowsAppRuntime.2
```

This matches the documented behavior of `MddBootstrapInitialize*`:

> The minor version is ignored for release 2.0+.

It also matches the 2.0 versioning design, where the package-family compatibility boundary was moved from `major.minor` to `major`.

---

## Consequence of this design

For an application built against Windows App SDK 2.0.x, the effective Bootstrap dependency becomes approximately:

```text
Package family:
Microsoft.WindowsAppRuntime.2

Minimum version:
2.0.x
```

If the machine has:

```text
2.0.x
2.1.x
2.4.x
```

then all of those packages belong to the same family and satisfy:

```text
Version >= MinVersion
```

Dynamic Dependency resolution can therefore select the highest applicable version.

There is no Bootstrap parameter for expressing:

```text
Version == 2.0.x
```

or:

```text
2.0.x <= Version < 2.1
```

or even:

```text
stay on the 2.0 minor line
```

The application therefore cannot reproduce the old 1.x behavior of selecting a particular `major.minor` Windows App Runtime family.

---

## Packaged and unpackaged applications take different initialization paths

This distinction is important.

### Packaged application

A packaged application declares its framework dependency in `Package.appxmanifest`.

For example:

```xml

```

Windows establishes the process' static package graph from the application package dependencies before the executable starts.

The Windows App SDK Bootstrapper is not responsible for selecting and adding the framework package.

This is also reflected directly in `MddBootstrapInitialize2()`:

```cpp
if (AppModel::Identity::IsPackagedProcess())
{
if (WI_IsFlagSet(
options,
MddBootstrapInitializeOptions_OnPackageIdentity_NOOP))
{
return S_OK;
}

hr = HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED);
}
else
{
hr = _MddBootstrapInitialize(
majorMinorVersion,
versionTag,
minVersion);
}
```

### Unpackaged application

An unpackaged application has no manifest-declared Windows App Runtime package dependency.

The Bootstrapper therefore dynamically constructs the package graph:

```text
unpackaged EXE starts

MddBootstrapInitialize2()

GetFrameworkPackageFamilyName()

TryCreatePackageDependency(
family,
minVersion)

AddPackageDependency()

Windows resolves a concrete packageFullName

framework package added to process package graph

DLL / WinRT / resource resolution uses that package
```

This means the actual Windows App Runtime version used by the executable is determined by Dynamic Dependency resolution, not simply by the NuGet package used at build time.

---

## The resolved version is explicitly tracked by the Bootstrapper

After `AddPackageDependency()` returns the concrete framework package, the Bootstrapper records its actual package version:

```cpp
const auto frameworkPackageIdentity{
::AppModel::Identity::PackageIdentity::FromPackageFullName(
packageFullName.get())
};

g_initializationFrameworkPackageVersion.Version =
frameworkPackageIdentity.Version().Version;
```

So the implementation itself distinguishes between:

```text
version requested at build/bootstrap time
```

and:

```text
framework package actually resolved by Windows
```

This is an important distinction because those versions can differ.

---

## This is not caused by a globally loaded runtime DLL

The behavior does not appear to be caused by a newer Windows App Runtime DLL already being loaded in another process and then being reused.

Windows framework packages support side-by-side use.

For example:

```text
Process A -> Windows App Runtime 2.0
Process B -> Windows App Runtime 2.4
```

can coexist.

A running process does not have its already established package graph changed when a newer framework package is installed.

The version change happens when a new process starts and the dependency is resolved again:

```text
process startup

package dependency resolution

concrete framework package selected

package added to package graph

DLL / WinRT resolution
```

Therefore the issue is about package dependency resolution, not global DLL reuse.

---

## Why the packaged/unpackaged difference needs clarification

The Windows App SDK 2.x design clearly intends all releases within the same major version to form one compatibility domain.

The Bootstrap implementation also clearly treats the minor version as irrelevant for 2.x.

However, in testing, packaged and unpackaged framework-dependent applications built against the same Windows App SDK version can resolve different Windows App Runtime versions under the same machine configuration.

The unpackaged path is explainable from the source code:

```text
Microsoft.WindowsAppRuntime.2
+
MinVersion
+
highest applicable Dynamic Dependency candidate
```

What is less clear is whether the difference from the packaged/static package graph is an intentional deployment behavior.

It would be useful to clarify:

1. Is an unpackaged Windows App SDK 2.x application intentionally expected to always roll forward to the highest compatible installed runtime in the same major version?

2. Is the packaged/static package graph expected to follow exactly the same version-selection policy?

3. If packaged and unpackaged applications can resolve different framework versions from equivalent version constraints, is that intentional?

4. Is there any supported way for an unpackaged application to select:

* a specific minor version;
* a maximum runtime version;
* or an exact framework package version?

5. If not, was the loss of the 1.x `major.minor` selection capability an explicit design decision?

---

## Minimal reproduction

1. Install Windows App SDK runtime 2.0.x.

2. Build both:

* a packaged framework-dependent application;
* an unpackaged framework-dependent application.

3. Both projects reference the same Windows App SDK 2.0.x NuGet package.

4. For the unpackaged application use:

```xml
None
false
```

5. Run both applications and record the actual framework package in each process package graph.

6. Install a newer stable Windows App Runtime 2.x, for example 2.4.x.

7. Completely terminate both applications.

8. Start the same binaries again without rebuilding.

9. Compare the resolved:

```text
Microsoft.WindowsAppRuntime.2____8wekyb3d8bbwe
```

package in each process graph.

10. For the unpackaged application, disable auto Bootstrap initialization:

```xml
false
```

and manually call:

```cpp
MddBootstrapInitialize2(
WINDOWSAPPSDK_RELEASE_MAJORMINOR,
WINDOWSAPPSDK_RELEASE_VERSION_TAG_W,
PACKAGE_VERSION{ .Version = WINDOWSAPPSDK_RUNTIME_VERSION_UINT64 },
MddBootstrapInitializeOptions_None);
```

11. Observe that the caller still cannot constrain resolution to the build-time minor version because:

* the minor part of `majorMinorVersion` is ignored for 2.x;
* `minVersion` is only a lower bound;
* no maximum or exact version is accepted.

---

## Expected behavior

Automatic roll-forward within a compatible major version is a reasonable default.

However, an unpackaged framework-dependent application should have an explicit opt-in mechanism for deterministic runtime selection.

For example:

```cpp
MddBootstrapInitialize3(
majorVersion,
versionTag,
minVersion,
maxVersion,
options);
```

or:

```cpp
enum class RuntimeVersionSelection
{
LatestCompatible,
MajorMinor,
Exact
};
```

This would preserve the current servicing model while allowing applications to request:

```text
latest compatible runtime
```

or:

```text
runtime from the build/tested minor line
```

or:

```text
exact runtime version
```

when required.

---

## Why this matters

The Windows App SDK 2.x model relies on the guarantee that newer releases within the same major version remain binary-compatible.

That makes automatic roll-forward useful in normal operation.

However, regressions can still occur.

Without any deterministic runtime-selection mechanism:

* the same already-built executable can run against different Windows App Runtime binaries depending on what has subsequently been installed;
* developers cannot reliably reproduce a customer issue against the original runtime;
* A/B testing two Windows App Runtime releases with the same application binary becomes difficult;
* applications have no temporary rollback mechanism when a regression is discovered;
* diagnosing whether a regression originates in the application or in a newer Windows App Runtime becomes harder;
* self-contained deployment becomes the only general way to guarantee exact runtime binaries, even when the application otherwise wants framework-dependent deployment.

This is especially significant for unpackaged applications because the Bootstrapper is the supported mechanism for constructing their Windows App Runtime package graph, yet the API currently exposes only a minimum version constraint.

---

## Real-world case that exposed this behavior

This behavior was initially noticed while investigating:

* apkipa/WUILiquidGlassDemo#1

That project relies on private/internal implementation details.

This issue is **not** requesting compatibility guarantees for private symbols, private ABI, undocumented interfaces, or hooks.

The project is relevant only because it made the runtime-selection behavior easy to observe:

```text
application built/tested against one Windows App SDK 2.x runtime

newer 2.x runtime installed

same unpackaged executable resolves to newer framework package
```

The underlying deployment question is independent of that project.

---

## Related references

* microsoft/WindowsAppSDK#6180 — Update Bootstrapper for WinAppSDK 2.x/SemVer
* https://github.com/microsoft/WindowsAppSDK/tree/main/dev/WindowsAppRuntime_BootstrapDLL/MddBootstrap.h
* https://github.com/microsoft/WindowsAppSDK/tree/main/dev/WindowsAppRuntime_BootstrapDLL/MddBootstrap.cpp
* https://github.com/microsoft/WindowsAppSDK/tree/main/dev/WindowsAppRuntime_BootstrapDLL/MddBootstrapAutoInitializer.cpp
* https://github.com/microsoft/WindowsAppSDK/tree/main/build/NuSpecs/Microsoft.WindowsAppSDK.BootstrapCommon.targets
* https://github.com/microsoft/WindowsAppSDK/tree/main/specs/Deployment/MSIXPackageVersioning.md
* https://github.com/microsoft/WindowsAppSDK/tree/main/specs/dynamicdependencies/DynamicDependencies.md
* microsoft/WindowsAppSDK#89 — MSIX Dynamic Dependencies
* apkipa/WUILiquidGlassDemo#1

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with Microsoft.WindowsAppSDK.BootstrapCommon.targets and the MddBootstrapInitialize2 and FirstTimeInitialization paths described in the issue. Reproduce the packaged and unpackaged applications with Windows App Runtime 2.0.x, then install a newer 2.x runtime and record each process package graph. Done means establishing and documenting the intended version-selection behavior and whether unpackaged apps have a supported exact or bounded version-selection mechanism.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
desktop
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.