Skip to content
WSLDeep Dive Published Updated 7 min readViews unavailable

.NET SDKs in WSL: Separate Linux Builds from Windows Toolchains

Run Linux-targeted .NET builds inside WSL with distro-owned SDKs, NuGet state, runtime identifiers, and explicit separation from Windows toolchains.

A .NET SDK installed in Windows and one installed inside a WSL distribution are not interchangeable merely because both expose a command named dotnet. They are different host executables with operating-system-specific dependencies, locations, environment variables, and workloads. A project may deliberately target either platform, but a Linux deployment is best validated with a Linux SDK and Linux build tools in WSL.

The important operational decision is to make the build’s host, target framework, runtime identifier, package source, and filesystem owner explicit. This prevents an IDE from selecting the Windows SDK while a shell expects Linux, and it keeps generated outputs from one platform from contaminating another.

Inspect the Linux SDK, not just the command name

Run these commands from the WSL distribution that owns the project:

command -v dotnet
dotnet --info
dotnet --list-sdks
dotnet --list-runtimes
dotnet --version

The executable path and the SDK list are more informative than dotnet –version alone. The latter reports the SDK selected for the current directory, which can be influenced by a global.json file. It does not tell you whether the same executable is running in another terminal or on Windows.

From PowerShell, inspect the Windows side independently:

Get-Command dotnet -All
dotnet --info

Do not assume the output should match. If the project is Linux-targeted, the Linux command must resolve to a Linux executable and its dotnet –info output should identify Linux. When using VS Code, install or enable the project tooling in the WSL remote context as well as the Windows UI context if the extension requires a server-side component.

Install from a deliberate Linux package source

Use the package source recommended for the current Linux distribution and .NET release. The current Microsoft installation guidance notes that Ubuntu’s own feeds provide supported .NET packages on recent Ubuntu releases, while the supported versions and feature bands vary by Ubuntu release. The correct choice is a distribution/version decision, not a universal apt install line.

Before mixing the Ubuntu and Microsoft package feeds, read the current .NET decision guide for that release. Competing repositories can offer the same package names with different availability and update behavior. If a specific SDK feature band is needed, record which repository supplied it and how upgrades are intended to work.

Install an SDK for development, not only a runtime. The SDK includes the tools required to restore, compile, and test projects. A runtime-only installation may run an already-built app but cannot satisfy a normal SDK build.

Let global.json select a reviewed SDK

A repository can declare a requested SDK version in global.json. That file constrains SDK selection; it does not install the requested SDK. If a build reports that no compatible SDK is found, compare the requested version and roll-forward policy with dotnet –list-sdks in the same working directory.

pwd
cat global.json
dotnet --list-sdks
dotnet --version
dotnet --info

Do not fix a missing SDK by installing arbitrary preview or unsupported builds on a shared machine. Determine whether the repository intentionally requires that version, whether CI uses the same SDK policy, and whether a supported patch or SDK band is available. Keep any SDK selection change in source control so developers and CI agree.

Keep the Linux workspace and package state on Linux

For a Linux build, store the checkout under the distro filesystem, such as /home/alice/src/service. Microsoft’s WSL guidance recommends keeping Linux-tool workloads in the Linux filesystem rather than under /mnt/c, where cross-boundary file operations can be slower or behave differently.

NuGet’s global package cache and SDK workloads are also owned by the Linux user environment. Do not copy a Windows NuGet cache or generated bin and obj directories into the WSL build and assume they are valid. Clean and restore on the target environment when debugging cross-platform build failures:

dotnet clean
dotnet restore
dotnet build --no-restore
dotnet test --no-build

Use dotnet clean only for generated project output that the project controls. If custom build targets write important data into bin or obj, inspect the project before deleting them. In continuous integration, a clean checkout is the best way to confirm that local caches are not hiding undeclared prerequisites.

Distinguish target framework from runtime identifier

A target framework moniker describes the API surface the project compiles against. A runtime identifier (RID) helps select runtime-specific assets and publish output. Neither setting changes the host operating system of the SDK process. A Linux SDK can cross-publish for some targets, but platform-specific native dependencies and workloads still matter.

Inspect project settings before making a deployment claim:

rg -n 'TargetFramework|RuntimeIdentifier|RuntimeIdentifiers|SelfContained' --glob '*.csproj' .
dotnet publish ./Service.csproj --configuration Release --runtime linux-x64

The example RID is illustrative only; choose a supported RID that matches the deployment architecture and current RID catalog. A self-contained publish bundles the .NET runtime for its target but does not remove dependencies on the Linux ABI or native libraries. A framework-dependent app still requires an appropriate target runtime at deployment.

For ARM64, musl-based distributions, or native packages, validate the exact target on a matching environment or in a matching container. A successful x64 build on an ARM Windows host is not proof that the emitted app supports ARM64, nor that it will run under every Linux distribution.

Diagnose native dependency and restore failures

Restore and build errors often reveal which layer is wrong. A missing NuGet package suggests feed, credentials, or dependency configuration; a missing shared library on launch points to the Linux runtime environment; an unsupported SDK points to selection or installation. Record the first causal error, not only the final nonzero exit.

Use a minimal diagnostic project or the existing test suite to verify the SDK and runtime separately. Check that network access to package feeds works from the distro, certificates are current, and any private feed configuration is available to the Linux user. Avoid placing credentials in project files or shell history. Use the team’s approved credential provider and inspect logs before sharing them.

If one project builds on Windows and fails on WSL, compare the selected SDK, target framework, RID, native libraries, case-sensitive paths, and generated artifacts. Filename casing can succeed on a case-insensitive Windows filesystem but fail in Linux. Do not “fix” that mismatch by placing a Linux build under Windows storage; correct the repository paths and test the intended target.

Make editor and command-line builds agree

An IDE is another client of the toolchain, not the authority for which SDK should be installed. In a WSL-connected editor window, confirm that the project folder is opened as a Linux path and run the same dotnet –info from its integrated terminal. If a build task launches from Windows, it may resolve a Windows SDK even though the source is mounted or browsed from WSL.

For repeatability, keep build/test commands in repository scripts or CI configuration. The editor should invoke those commands in the WSL execution context. Record the exact working directory and SDK selection when debugging, since both can change resolution.

Acceptance checks

A Linux .NET workflow is ready when a clean WSL checkout selects the repository’s intended SDK, restores from approved package sources, builds and tests using Linux tools, and publishes for an explicit supported target. Confirm that the executable path is Linux-owned, generated artifacts were produced inside WSL, and the resulting app runs in a compatible Linux environment.

Capture the distro release, architecture, SDK list, global.json, package-feed policy, and publish RID with the build record. Keep Windows SDK validation separate if the project also ships Windows binaries. The goal is parity where intended, not artificial equality between distinct runtimes.

Related:

Sources:

Comments