Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4cc46e3
Add Unix shell profile modification for defaultinstall
dsplaisted Mar 11, 2026
41d8f9b
Include dotnetup directory in PATH in generated env scripts
dsplaisted Mar 13, 2026
955ca12
Implement defaultinstall admin on Unix with dotnetup-only profiles
dsplaisted Mar 13, 2026
9729e07
Rewrite unix-environment-setup.md to lead with user-facing workflows
dsplaisted Mar 15, 2026
2b754e2
Remove no-op SetEnvironmentVariable calls on Unix
dsplaisted Mar 15, 2026
c2fa1ba
Use eval consistently in print-env-script examples
dsplaisted Mar 15, 2026
8ab0cf1
Remove misleading 'source this script' comments from generated env sc…
dsplaisted Mar 15, 2026
04d84b7
Update script examples to include dotnetup directory in PATH
dsplaisted Mar 15, 2026
3b0b5f7
Update future work: defaultinstall admin is implemented
dsplaisted Mar 15, 2026
ee26614
Skip default install prompt when shell is unsupported on Unix
dsplaisted Mar 15, 2026
fd9b8c9
Consolidate shell provider list into ShellDetection
dsplaisted Mar 15, 2026
e00539e
Merge AddProfileEntries and ReplaceProfileEntries into one method
dsplaisted Mar 15, 2026
0605491
Use single MarkerComment constant from ShellProfileManager
dsplaisted Mar 15, 2026
f001d3d
Clear shell command cache in generated env scripts
dsplaisted Mar 15, 2026
ee3acb8
Inline ShellOption validators and completions into initializer
dsplaisted Mar 15, 2026
7216bba
Move shell types to Microsoft.DotNet.Tools.Bootstrapper.Shell namespace
dsplaisted Mar 15, 2026
b3afd4e
Fix E2E test PATH assertion to handle dotnetup directory in PATH
dsplaisted Mar 15, 2026
156706d
Support multiple component-specs in runtime install
dsplaisted Mar 15, 2026
e9be3b0
Add concurrent download support for multi-runtime install
dsplaisted Mar 15, 2026
81006c5
Add tests for multi-spec parsing and InstallMultiple
dsplaisted Mar 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
201 changes: 132 additions & 69 deletions documentation/general/dotnetup/unix-environment-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,98 @@

## Overview

This document describes the design for setting up the .NET environment via initialization scripts using the `dotnetup print-env-script` command. This is the first step toward enabling automatic user profile configuration for Unix as described in [issue #51582](https://github.com/dotnet/sdk/issues/51582). Note that this also supports PowerShell and thus Windows, but on Windows the main method of configuring the environment will be to set environment variables which are stored in the registry instead of written by initialization scripts.
dotnetup automatically configures the Unix shell environment so that .NET is available in every new terminal session. This involves modifying shell profile files to set the `PATH` and `DOTNET_ROOT` environment variables. The same mechanism also supports PowerShell on any platform.

## Background
On Windows the primary method is registry-based environment variables, which is handled separately. This document focuses on the Unix (and PowerShell) profile-based approach.

The dotnetup tool manages multiple .NET installations in a local user hive. For .NET to be accessible from the command line, the installation directory must be:
1. Added to the `PATH` environment variable
2. Set as the `DOTNET_ROOT` environment variable
## How the Environment Gets Configured

On Unix systems, this requires modifying shell configuration files (like `.bashrc`, `.zshrc`, etc.) or sourcing environment setup scripts.
There are two primary ways the environment is configured:

## Design Goals
### 1. During `dotnetup sdk install` / `dotnetup runtime install`

1. **Non-invasive**: Don't automatically modify user shell configuration files without explicit consent
2. **Flexible**: Support multiple shells (bash, zsh, PowerShell)
3. **Reversible**: Users should be able to easily undo environment changes
4. **Single-file execution**: Generate scripts that can be sourced or saved for later use
5. **Discoverable**: Make it easy for users to understand how to configure their environment
When running interactively (the default in a terminal), the install command prompts the user to set the default install if one is not already configured:

## The `dotnetup print-env-script` Command
```
Do you want to set the install path (~/.local/share/dotnet) as the default dotnet install?
This will update the PATH and DOTNET_ROOT environment variables. [Y/n]
```

If the user confirms (or passes `--set-default-install` explicitly):

- **On Windows**: Environment variables are set in the registry and updated for the current process.
- **On Unix**: Shell profile files are modified so .NET is available in future terminal sessions. Since profile changes only take effect in new shells, dotnetup also prints an activation command for the current terminal:

```
To start using .NET in this terminal, run:
eval "$('/home/user/.local/share/dotnetup/dotnetup' print-env-script --shell bash)"
```

If the default install is already fully configured and matches the install path, the prompt is skipped entirely.

### 2. `dotnetup defaultinstall`

A standalone command that explicitly configures (or reconfigures) the default .NET install:

```bash
# Set up user-level default install (modifies shell profiles)
dotnetup defaultinstall user

# Switch to admin/system-managed .NET (removes DOTNET_ROOT from profiles, keeps dotnetup on PATH)
dotnetup defaultinstall admin
```

**`defaultinstall user`** on Unix:
1. Detects the current shell
2. Modifies the appropriate shell profile files
3. Prints an activation command for the current terminal

**`defaultinstall admin`** on Unix:
- Replaces existing profile entries with dotnetup-only entries (keeps dotnetup on PATH but removes `DOTNET_ROOT` and dotnet from `PATH`), since the system package manager owns the .NET installation.

## Shell Profile Modification

### Which Profile Files Are Modified

| Shell | Files modified | Rationale |
|-------|---------------|-----------|
| **bash** | `~/.bashrc` (always) + the first existing of `~/.bash_profile` / `~/.profile` (creates `~/.profile` if neither exists) | `.bashrc` covers Linux terminals (non-login shells). The login profile covers macOS Terminal and SSH sessions. We never create `~/.bash_profile` to avoid shadowing an existing `~/.profile`. |
| **zsh** | `~/.zshrc` (created if needed) | Covers all interactive zsh sessions. `~/.zshenv` is avoided because on macOS, `/etc/zprofile` runs `path_helper` which resets PATH after `.zshenv` loads. |
| **pwsh** | `~/.config/powershell/Microsoft.PowerShell_profile.ps1` (creates directory and file if needed) | Standard `$PROFILE` path on Unix. |

### Profile Entry Format

Each profile file gets a marker comment and an eval line:

**Bash / Zsh:**
```bash
# dotnetup
eval "$('/path/to/dotnetup' print-env-script --shell bash)"
```

**PowerShell:**
```powershell
# dotnetup
& '/path/to/dotnetup' print-env-script --shell pwsh | Invoke-Expression
```

The path to dotnetup is the full path to the running binary (`Environment.ProcessPath`).

### Backups

Before modifying an existing profile file, dotnetup creates a backup (e.g., `~/.bashrc.dotnetup-backup`). This allows the user to restore the file if needed.

### Reversibility

To remove the environment configuration, find the `# dotnetup` marker comment and the line immediately after it in each profile file, and remove both lines. The backup files can be used as a reference.

### Idempotency

If a profile file already contains the `# dotnetup` marker, the entry is not duplicated.

## The `print-env-script` Command

`print-env-script` is the low-level building block that generates shell-specific environment scripts. It is called internally by profile entries and activation commands, but can also be used standalone for custom setups, CI pipelines, or when you want to source the environment without modifying profile files.

### Command Structure

Expand All @@ -40,14 +113,9 @@ dotnetup print-env-script [--shell <shell>] [--dotnet-install-path <path>]

### Usage Examples

#### Auto-detect current shell
```bash
dotnetup print-env-script
```

#### Generate and source in one command
#### Eval directly (one-time, current terminal only)
```bash
source <(dotnetup print-env-script)
eval "$(dotnetup print-env-script)"
```

#### Explicitly specify shell
Expand All @@ -59,97 +127,93 @@ dotnetup print-env-script --shell zsh
```bash
dotnetup print-env-script --shell bash > ~/.dotnet-env.sh
# Later, in .bashrc or manually:
source ~/.dotnet-env.sh
. ~/.dotnet-env.sh
```

#### Use custom installation path
```bash
dotnetup print-env-script --dotnet-install-path /opt/dotnet
```

## Generated Script Format
### Generated Script Format

The command generates shell-specific scripts that:
1. Set the `DOTNET_ROOT` environment variable to the installation path
2. Prepend the installation path to the `PATH` environment variable
3. Clear the shell's cached command location for `dotnet` to pick up the new PATH

### Bash/Zsh Example
**Bash/Zsh Example:**
```bash
#!/usr/bin/env bash
# This script configures the environment for .NET installed at /home/user/.local/share/dotnet
# Source this script to add .NET to your PATH and set DOTNET_ROOT

export DOTNET_ROOT='/home/user/.local/share/dotnet'
export PATH='/home/user/.local/share/dotnet':$PATH
export PATH='/home/user/.local/share/dotnetup':'/home/user/.local/share/dotnet':$PATH
hash -d dotnet 2>/dev/null
hash -d dotnetup 2>/dev/null
```

### PowerShell Example
**PowerShell Example:**
```powershell
# This script configures the environment for .NET installed at /home/user/.local/share/dotnet
# Source this script (dot-source) to add .NET to your PATH and set DOTNET_ROOT
# Example: . ./dotnet-env.ps1

$env:DOTNET_ROOT = '/home/user/.local/share/dotnet'
$env:PATH = '/home/user/.local/share/dotnet' + [IO.Path]::PathSeparator + $env:PATH
$env:PATH = '/home/user/.local/share/dotnetup' + [IO.Path]::PathSeparator + '/home/user/.local/share/dotnet' + [IO.Path]::PathSeparator + $env:PATH
```

### Shell Detection

When `--shell` is not specified, the command automatically detects the current shell:

1. **On Unix**: Reads the `$SHELL` environment variable and extracts the shell name from the path (e.g., `/bin/bash` → `bash`)
2. **On Windows**: Defaults to PowerShell (`pwsh`)

### Security Considerations

All installation paths are properly escaped to prevent shell injection vulnerabilities:
- **Bash/Zsh**: Uses single quotes with `'\''` escaping for embedded single quotes
- **PowerShell**: Uses single quotes with `''` escaping for embedded single quotes

This ensures that paths containing special characters, spaces, or shell metacharacters are handled safely.

## Implementation Details

### Provider Model

The implementation uses a provider model similar to `System.CommandLine.StaticCompletions`, making it easy to add support for additional shells in the future.
The implementation uses a provider model, making it easy to add support for additional shells in the future.

**Interface**: `IEnvShellProvider`
```csharp
public interface IEnvShellProvider
{
string ArgumentName { get; } // Shell name for CLI (e.g., "bash")
string Extension { get; } // File extension (e.g., "sh")
string? HelpDescription { get; } // Help text for the shell
string GenerateEnvScript(string dotnetInstallPath);
string ArgumentName { get; }
string Extension { get; }
string? HelpDescription { get; }
string GenerateEnvScript(string dotnetInstallPath, string? dotnetupDir = null, bool includeDotnet = true);
IReadOnlyList<string> GetProfilePaths();
string GenerateProfileEntry(string dotnetupPath, bool dotnetupOnly = false);
string GenerateActivationCommand(string dotnetupPath, bool dotnetupOnly = false);
}
```

**Implementations**:
- `BashEnvShellProvider`: Generates bash-compatible scripts
- `ZshEnvShellProvider`: Generates zsh-compatible scripts
- `PowerShellEnvShellProvider`: Generates PowerShell Core scripts

### Shell Detection
**Implementations**: `BashEnvShellProvider`, `ZshEnvShellProvider`, `PowerShellEnvShellProvider`

The command automatically detects the current shell when the `--shell` option is not provided:
### ShellDetection

1. **On Unix**: Reads the `$SHELL` environment variable and extracts the shell name from the path
- Example: `/bin/bash` → `bash`
2. **On Windows**: Defaults to PowerShell (`pwsh`)
`ShellDetection.GetCurrentShellProvider()` resolves the user's current shell to the matching `IEnvShellProvider`. On Windows it returns the PowerShell provider; on Unix it reads `$SHELL`.

### Security Considerations
### ShellProfileManager

**Path Escaping**: All installation paths are properly escaped to prevent shell injection vulnerabilities:
- **Bash/Zsh**: Uses single quotes with `'\''` escaping for embedded single quotes
- **PowerShell**: Uses single quotes with `''` escaping for embedded single quotes

This ensures that paths containing special characters, spaces, or shell metacharacters are handled safely.

## Advantages of Generated Scripts

As noted in the discussion, generating scripts dynamically has several advantages over using embedded resource files:

1. **Single-file execution**: Users can source the script directly from the command output without needing to extract files
2. **Flexibility**: Easy to customize the installation path or add future features
3. **No signing required**: Generated text doesn't require code signing, unlike downloaded executables or scripts
4. **Immediate availability**: No download or extraction step needed
5. **Transparency**: Users can easily inspect what the script does by running the command
`ShellProfileManager` coordinates profile file modifications:
- `AddProfileEntries(provider, dotnetupPath)` — appends entries, creates backups, skips if already present
- `RemoveProfileEntries(provider)` — finds and removes marker + eval lines
- `ReplaceProfileEntries(provider, dotnetupPath, dotnetupOnly)` — removes then adds (used by `defaultinstall admin`)

## Future Work

This command provides the foundation for future enhancements:

1. **Automatic profile modification**: Add a command to automatically update shell configuration files (`.bashrc`, `.zshrc`, etc.) with user consent
2. **Profile backup**: Create backups of shell configuration files before modification
3. **Uninstall/removal**: Add commands to remove dotnetup configuration from shell profiles
4. **Additional shells**: Support for fish, tcsh, and other shells
5. **Environment validation**: Commands to verify that the environment is correctly configured
1. **System-wide configuration on Unix**: Writing to system-wide locations like `/etc/profile.d/` for admin installs is not yet supported.
2. **Additional shells**: Support for fish, tcsh, and other shells.
3. **Environment validation**: Commands to verify that the environment is correctly configured.

## Related Issues

Expand All @@ -163,5 +227,4 @@ The implementation includes comprehensive tests:
- Shell provider tests for script generation
- Security tests for special character handling
- Help documentation tests

All tests ensure that the generated scripts are syntactically correct and properly escape paths.
- Shell profile manager tests for add/remove/idempotency/backup behavior
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ internal class DotnetArchiveExtractor : IDisposable
private readonly IProgressTarget _progressTarget;
private readonly IArchiveDownloader _archiveDownloader;
private readonly bool _shouldDisposeDownloader;
private readonly bool _ownsReporter;
private MuxerHandler? MuxerHandler { get; set; }
private string? _archivePath;
private IProgressReporter? _progressReporter;
Expand All @@ -29,11 +30,14 @@ public DotnetArchiveExtractor(
ReleaseVersion resolvedVersion,
ReleaseManifest releaseManifest,
IProgressTarget progressTarget,
IArchiveDownloader? archiveDownloader = null)
IArchiveDownloader? archiveDownloader = null,
IProgressReporter? sharedReporter = null)
{
_request = request;
_resolvedVersion = resolvedVersion;
_progressTarget = progressTarget;
_progressReporter = sharedReporter;
_ownsReporter = sharedReporter is null;
ScratchDownloadDirectory = Directory.CreateTempSubdirectory().FullName;

if (archiveDownloader != null)
Expand Down Expand Up @@ -468,8 +472,11 @@ public void Dispose()
{
try
{
// Dispose the progress reporter to finalize progress display
_progressReporter?.Dispose();
// Dispose the progress reporter to finalize progress display (only if we own it)
if (_ownsReporter)
{
_progressReporter?.Dispose();
}
}
catch
{
Expand Down
Loading
Loading