UseArtifactsOutput + multi-target Android library: NETSDK1047 → MSB3030 → XALNS7028 chain in inner per-RID build
- Dominant language
- C#
- Stars
- 2.1k
- Forks
- 579
- Avg merge
- 1d 19h
- Merged PRs (30d)
- 252
Description
## Summary
When a repo sets `UseArtifactsOutput=true`, a class library multi-targets `net10.0;net10.0-android36.1`, and an Android application (`AndroidApplication=true`) references it, `dotnet build` on the solution fails with a chain of three errors. Each one can be worked around independently, exposing the next link in the chain. The third (`XALNS7028`) comes from `LinkAssembliesNoShrink`; the first two are from the .NET SDK / Microsoft.Common.targets, but all three cascade from the same Android-SDK-driven inner per-RID library build (`_ComputeFilesToPublishForRuntimeIdentifiers`).
I could not reproduce without `UseArtifactsOutput=true`. With the legacy `bin/`/`obj/` layout the chain does not fire.
## Environment
- .NET SDK `11.0.100-preview.4.26230.115`
- Workloads: `android 36.99.0-preview.4.137/11.0.100-preview.4`, `maui-windows`, `ios`, `maccatalyst`
- `Microsoft.Android.Sdk.Windows` `36.1.53`
- Windows 11
- `UseArtifactsOutput=true` set at repo root in `Directory.Build.props`
## Minimal repro (10 files, inline)
Build with `dotnet build Repro.slnx -c Debug` from the repro root.
### `Directory.Build.props`
```xml
true
enable
latest
```
### `Directory.Build.targets`
Empty by default. The three workarounds (uncomment one at a time to walk the chain):
```xml
android-arm64;android-x64
<_RidAssemblyPath>@(IntermediateAssembly)
<_RidRefAssemblyPath>@(IntermediateRefAssembly)
<_NonRidAssemblyPath>$([System.String]::new('$(_RidAssemblyPath)').Replace('_$(RuntimeIdentifier)\', '\').Replace('_$(RuntimeIdentifier)/', '/'))
<_NonRidRefAssemblyPath>$([System.String]::new('$(_RidRefAssemblyPath)').Replace('_$(RuntimeIdentifier)\', '\').Replace('_$(RuntimeIdentifier)/', '/'))
$(OutDir)$(TargetFileName)
```
### `Repro.slnx`
```xml
```
### `MyLib/MyLib.csproj`
```xml
net10.0;net10.0-android36.1
```
### `MyLib/Greeter.cs`
```csharp
namespace MyLib;
public class Greeter
{
public string Hello() =>
#if ANDROID
"Hello from Android";
#else
"Hello from Desktop";
#endif
}
```
### `MyApp/MyApp.csproj`
```xml
net10.0;net10.0-android36.1
Exe
true
android-arm64;android-x64
com.example.xalns7028
23
```
### `MyApp/Program.cs`
```csharp
#if !ANDROID
System.Console.WriteLine(new MyLib.Greeter().Hello());
#endif
```
### `MyApp/MainActivity.cs`
```csharp
#if ANDROID
using Android.App;
using Android.OS;
namespace MyApp;
[Activity(Label = "Repro", MainLauncher = true)]
public class MainActivity : Activity
{
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
System.Console.WriteLine(new MyLib.Greeter().Hello());
}
}
#endif
```
## Error chain
### 1. `NETSDK1047` (no workarounds)
```
error NETSDK1047: Assets file '.../artifacts/obj/MyLib/project.assets.json'
doesn't have a target for 'net10.0-android36.1/android-arm64'. Ensure that
restore has run and that you have included 'net10.0-android36.1' in the
TargetFrameworks for your project. You may also need to include 'android-arm64'
in your project's RuntimeIdentifiers.
[.../MyLib/MyLib.csproj::TargetFramework=net10.0-android36.1]
```
`MyLib` is a library — `RuntimeIdentifiers` is not set in its csproj (libraries don't normally need it). When the Android SDK's inner per-RID build of `MyLib` runs (kicked off by `MyApp`'s apk pipeline via `_ComputeFilesToPublishForRuntimeIdentifiers`), `ResolvePackageAssets` requires `net10.0-android36.1/android-arm64` in `project.assets.json`, but NuGet restore only produced `net10.0-android36.1` (no RID) for the library.
**Workaround 1** (`_SetRIDsForRestoreProjectSpec`) forces `RuntimeIdentifiers=android-arm64;android-x64` on the library during restore-spec generation, so the assets file gets the per-RID targets the SDK will later ask for.
### 2. `MSB3030` (after workaround 1)
```
error MSB3030: Could not copy the file
".../artifacts/obj/MyLib/release_net10.0-android36.1_android-x64/MyLib.dll"
because it was not found.
[.../MyLib/MyLib.csproj::TargetFramework=net10.0-android36.1]
```
The Android SDK invokes the inner per-RID library build with:
- `AppendRuntimeIdentifierToOutputPath=true` — shifts `OutputPath`/`OutDir` to a RID-suffixed path
- `SkipCompilerExecution=true` — Roslyn exits immediately; no dll is produced
- `_OuterOutputPath=` — `AssemblyResolution.targets` PropertyGroup overrides `OutDir` at evaluation time
`AppendRuntimeIdentifierToOutputPath=true` also shifts `@(IntermediateAssembly)` to the RID-suffixed `obj` path. `SkipCompilerExecution=true` means no dll is written there. `CopyFilesToOutputDirectory` then tries to copy `@(IntermediateAssembly)` from that path → MSB3030.
**Workaround 2** (`_SeedRidIntermediateAssemblyForAndroidLibraries`) copies the already-built non-RID assembly (produced by the outer multi-target build of `MyLib`) into the RID-suffixed `obj` dir so `CopyFilesToOutputDirectory` has a source.
### 3. `XALNS7028` (after workarounds 1 + 2)
```
error XALNS7028: System.IO.FileNotFoundException: Could not load assembly
'MyLib, Version=0.0.0.0, Culture=neutral, PublicKeyToken='.
Perhaps it doesn't exist in the Mono for Android profile?
File name: 'MyLib.dll'
at Java.Interop.Tools.Cecil.DirectoryAssemblyResolver.Resolve(AssemblyNameReference reference, ReaderParameters parameters)
at Xamarin.Android.Tasks.AssemblyModifierPipeline.RunPipeline(AssemblyPipeline pipeline, ITaskItem source, ITaskItem destination)
at Xamarin.Android.Tasks.AssemblyModifierPipeline.RunTask()
at Microsoft.Android.Build.Tasks.AndroidTask.Execute()
[.../MyApp/MyApp.csproj::TargetFramework=net10.0-android36.1]
```
After workaround 2 the dll exists in `obj`, gets copied to `bin`, and `CopyFilesToOutputDirectory` succeeds. But the resolver in `LinkAssembliesNoShrink` still can't find `MyLib.dll` — even though it is right there in `MyApp`'s outer bin.
Root cause: `TargetPath` is a static property set at project evaluation time from `OutDir + TargetFileName`. The SDK's `DefaultOutputPaths.targets` sets it first using the RID-suffixed `ArtifactsPivots`. The Android SDK's `AssemblyResolution.targets` then overrides `OutDir` to `$(_OuterOutputPath)` via a top-level `PropertyGroup` — but the `TargetPath` PropertyGroup is conditioned on `'$(TargetPath)' == ''` and no longer fires. `GetTargetPath` returns the stale RID-suffixed library bin path (which never has a dll), and that path ends up in `@(ResolvedAssemblies)` as a search directory. The dll isn't there, and Cecil throws `FileNotFoundException`.
**Workaround 3** (`_FixLibraryTargetPathForInnerRidBuild`) runs `BeforeTargets="GetTargetPathWithTargetPlatformMoniker"` and forcibly sets `TargetPath` from the corrected `OutDir`. `@(ResolvedAssemblies)` then points at the right directory and the resolver succeeds.
`BeforeTargets="GetTargetPathWithTargetPlatformMoniker"` rather than `GetTargetPath` because in CLI builds `ResolveProjectReferences` calls `GetTargetPathWithTargetPlatformMoniker` directly, and in VS builds `GetTargetPath` triggers it as a `BeforeTargets` — in both cases we must update `TargetPath` before it's captured into `@(TargetPathWithTargetPlatformMoniker)`.
## Asymmetry with VS
`dotnet build solution.slnx` from the CLI triggers the apk pipeline for Android apps and fires the full chain. `dotnet build` of an Android app project alone (or of a single TFM) often doesn't reach `LinkAssembliesNoShrink` because the apk packaging targets are skipped — so the same project that fails in VS Rebuild All may appear to build clean on a focused CLI build. This makes the bug easy to miss until someone opens the solution in VS.
## Proposed fixes (three independent root causes)
1. **NuGet restore for multi-target Android libraries.** When a library project's `TargetFrameworks` includes an Android moniker, `_GenerateRestoreProjectSpec` could default `RuntimeIdentifiers` to the platform RIDs (`android-arm64;android-x64`, plus `android-arm` / `android-x86` for older targets) so the assets file gets the per-RID targets the SDK will later ask for. The library author shouldn't need to know that consumers will trigger per-RID builds.
2. **`SkipCompilerExecution=true` + `AppendRuntimeIdentifierToOutputPath=true`.** `CopyFilesToOutputDirectory` should either skip when there is no compile output, or the Android SDK should not shift `IntermediateAssembly` to a RID-suffixed `obj` path that nothing writes to.
3. **`_OuterOutputPath` override leaks stale `TargetPath`.** When the Android SDK overrides `OutDir` after project evaluation, `TargetPath` should be recomputed — either by re-running the `DefaultOutputPaths.targets` PropertyGroup, or by changing `GetTargetPath` to derive from `OutDir` at execution time instead of using the cached `TargetPath` property. Otherwise stale paths leak into `@(ResolvedAssemblies)` and the assembly resolver fails for assemblies that are physically present at the new location.
(Filed from the field — happy to test patches against this repro.)
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.