Skip to content

Commit 4b95eb5

Browse files
Unify scoped install CLI across platforms
- add Unix all-users support with shared system bin layouts - align scope syntax with winget-style user/machine values - update docs and CLI tests for the unified scoped surface Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 802f488 commit 4b95eb5

5 files changed

Lines changed: 2271 additions & 148 deletions

File tree

README.md

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,57 @@ pwsh-7.4 --version
5151
pwsh-7.5 --version
5252
```
5353

54+
## Scoped installs
55+
56+
`multi-pwsh install`, `update`, `uninstall`, and `list` now support `--scope <user|machine>` across Windows, macOS, and Linux.
57+
58+
That means:
59+
60+
- extracted versions stay side-by-side under the selected install root
61+
- aliases continue to live in one stable bin directory
62+
- PATH only needs one entry per scope
63+
- `user` is the default scope when `--scope` is omitted
64+
65+
Platform behavior:
66+
67+
- Windows uses the GitHub ZIP archives with MSI-like install roots and optional installer-style integrations.
68+
- macOS `machine` installs use the official `.tar.gz` archives under `/usr/local/microsoft/powershell` with aliases published to `/usr/local/bin`.
69+
- Linux `machine` installs use the official `.tar.gz` archives under `/opt/microsoft/powershell` with aliases published to `/usr/local/bin`.
70+
- Unix `machine` installs expect you to provide elevation yourself; `multi-pwsh` does not invoke `sudo`.
71+
72+
Examples:
73+
74+
```powershell
75+
multi-pwsh install 7.4
76+
multi-pwsh install 7.5 --scope machine --enable-psremoting --add-explorer-context-menu
77+
multi-pwsh install 7.5 --scope machine
78+
multi-pwsh list --scope all
79+
multi-pwsh uninstall 7.4.13 --scope machine
80+
```
81+
82+
Windows scoped-install flags mirror the most useful MSI-style options:
83+
84+
- `--add-path` / `--no-add-path`
85+
- `--register-manifest` / `--no-register-manifest`
86+
- `--enable-psremoting`
87+
- `--disable-telemetry`
88+
- `--add-explorer-context-menu`
89+
- `--add-file-context-menu`
90+
- `--use-mu` / `--no-use-mu`
91+
- `--enable-mu` / `--no-enable-mu`
92+
- `--scope <user|machine>`
93+
- `--root <path>`
94+
95+
On macOS and Linux, scoped installs support:
96+
97+
- `--scope <user|machine>`
98+
- `--root <path>`
99+
- `--arch <auto|x64|x86|arm64|arm32>`
100+
- `--include-prerelease`
101+
- `--add-path` / `--no-add-path`
102+
103+
The Windows-only integration flags above currently return an error on macOS/Linux.
104+
54105
## Manage installed lines
55106

56107
```powershell
@@ -79,10 +130,10 @@ multi-pwsh doctor --repair-aliases
79130
`multi-pwsh` usage reference:
80131

81132
```text
82-
multi-pwsh install <version|major|major.minor|major.minor.x> [--arch <auto|x64|x86|arm64|arm32>] [--include-prerelease]
83-
multi-pwsh update <major.minor> [--arch <auto|x64|x86|arm64|arm32>] [--include-prerelease]
84-
multi-pwsh uninstall <version> [--force]
85-
multi-pwsh list [--available] [--include-prerelease]
133+
multi-pwsh install <version|major|major.minor|major.minor.x> [--scope <user|machine>] [--root <path>] [--arch <auto|x64|x86|arm64|arm32>] [--include-prerelease] [--add-path|--no-add-path] [--register-manifest|--no-register-manifest] [--enable-psremoting] [--disable-telemetry] [--add-explorer-context-menu] [--add-file-context-menu] [--use-mu|--no-use-mu] [--enable-mu|--no-enable-mu]
134+
multi-pwsh update <major.minor> [--scope <user|machine>] [--root <path>] [--arch <auto|x64|x86|arm64|arm32>] [--include-prerelease] [--add-path|--no-add-path] [--register-manifest|--no-register-manifest] [--enable-psremoting] [--disable-telemetry] [--add-explorer-context-menu] [--add-file-context-menu] [--use-mu|--no-use-mu] [--enable-mu|--no-enable-mu]
135+
multi-pwsh uninstall <version> [--scope <user|machine>] [--root <path>] [--force]
136+
multi-pwsh list [--scope <user|machine|all>] [--root <path>] [--available] [--include-prerelease]
86137
multi-pwsh venv create <name>
87138
multi-pwsh venv delete <name>
88139
multi-pwsh venv export <name> <archive.zip>
@@ -94,6 +145,8 @@ multi-pwsh host <version|major|major.minor|pwsh-alias> [-VirtualEnvironment <nam
94145
multi-pwsh doctor --repair-aliases
95146
```
96147

148+
The MSI-style integration flags in the `install` and `update` forms are Windows-only; on macOS/Linux, use `--scope`, `--root`, `--arch`, `--include-prerelease`, and `--add-path` controls. Legacy scope aliases such as `current-user` and `all-users` are still accepted for compatibility.
149+
97150
### Venv cmdlet matrix tests (Pester)
98151

99152
Use the local Pester harness to validate venv-sensitive cmdlet behavior across installed version aliases (`pwsh-x.y.z`).

crates/multi-pwsh/src/layout.rs

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub struct InstallLayout {
1212
bin_dir: PathBuf,
1313
cache_dir: PathBuf,
1414
venvs_dir: PathBuf,
15+
versions_dir: PathBuf,
1516
os: HostOs,
1617
}
1718

@@ -30,12 +31,47 @@ impl InstallLayout {
3031
let venvs_dir = env::var_os("MULTI_PWSH_VENV_DIR")
3132
.map(PathBuf::from)
3233
.unwrap_or_else(|| home.join("venv"));
34+
let versions_dir = home.join("multi");
3335

3436
Ok(InstallLayout {
3537
home,
3638
bin_dir,
3739
cache_dir,
3840
venvs_dir,
41+
versions_dir,
42+
os,
43+
})
44+
}
45+
46+
pub fn from_root(os: HostOs, home: PathBuf) -> Result<Self> {
47+
Self::from_root_with_versions_dir(os, home.clone(), home.join("multi"))
48+
}
49+
50+
pub fn from_root_with_versions_dir(os: HostOs, home: PathBuf, versions_dir: PathBuf) -> Result<Self> {
51+
Self::from_parts(
52+
os,
53+
home.clone(),
54+
home.join("bin"),
55+
home.join("cache"),
56+
home.join("venv"),
57+
versions_dir,
58+
)
59+
}
60+
61+
pub fn from_parts(
62+
os: HostOs,
63+
home: PathBuf,
64+
bin_dir: PathBuf,
65+
cache_dir: PathBuf,
66+
venvs_dir: PathBuf,
67+
versions_dir: PathBuf,
68+
) -> Result<Self> {
69+
Ok(InstallLayout {
70+
home,
71+
bin_dir,
72+
cache_dir,
73+
venvs_dir,
74+
versions_dir,
3975
os,
4076
})
4177
}
@@ -65,7 +101,7 @@ impl InstallLayout {
65101
}
66102

67103
pub fn versions_dir(&self) -> PathBuf {
68-
self.home.join("multi")
104+
self.versions_dir.clone()
69105
}
70106

71107
pub fn preferred_version_dir(&self, version: &Version) -> PathBuf {
@@ -80,6 +116,10 @@ impl InstallLayout {
80116
self.os.executable_name()
81117
}
82118

119+
pub fn os(&self) -> HostOs {
120+
self.os
121+
}
122+
83123
pub fn version_dir(&self, version: &Version) -> PathBuf {
84124
let preferred = self.preferred_version_dir(version);
85125
if preferred.exists() {
@@ -317,4 +357,71 @@ mod tests {
317357
assert_eq!(layout.installed_versions().unwrap(), vec![new_version, legacy_version]);
318358
});
319359
}
360+
361+
#[test]
362+
fn from_root_ignores_multi_pwsh_env_overrides() {
363+
let temp_dir = TempDir::new().unwrap();
364+
let explicit_home = temp_dir.path().join("package-root");
365+
let ignored_home = temp_dir.path().join("ignored-home");
366+
let overridden_bin = temp_dir.path().join("override-bin");
367+
let overridden_cache = temp_dir.path().join("override-cache");
368+
let overridden_venv = temp_dir.path().join("override-venv");
369+
370+
with_layout_env(
371+
Some(&ignored_home),
372+
Some(&overridden_bin),
373+
Some(&overridden_cache),
374+
Some(&overridden_venv),
375+
|| {
376+
let layout = InstallLayout::from_root(HostOs::Windows, explicit_home.clone()).unwrap();
377+
assert_eq!(layout.home(), explicit_home.as_path());
378+
assert_eq!(layout.bin_dir(), explicit_home.join("bin"));
379+
assert_eq!(layout.cache_dir(), explicit_home.join("cache"));
380+
assert_eq!(layout.venvs_dir(), explicit_home.join("venv"));
381+
assert_eq!(layout.versions_dir(), explicit_home.join("multi"));
382+
},
383+
);
384+
}
385+
386+
#[test]
387+
fn from_root_with_versions_dir_supports_direct_version_roots() {
388+
let temp_dir = TempDir::new().unwrap();
389+
let explicit_home = temp_dir.path().join("package-root");
390+
391+
let layout =
392+
InstallLayout::from_root_with_versions_dir(HostOs::Windows, explicit_home.clone(), explicit_home.clone())
393+
.unwrap();
394+
395+
assert_eq!(layout.home(), explicit_home.as_path());
396+
assert_eq!(layout.bin_dir(), explicit_home.join("bin"));
397+
assert_eq!(layout.cache_dir(), explicit_home.join("cache"));
398+
assert_eq!(layout.venvs_dir(), explicit_home.join("venv"));
399+
assert_eq!(layout.versions_dir(), explicit_home);
400+
}
401+
402+
#[test]
403+
fn from_parts_supports_custom_bin_and_versions_dirs() {
404+
let temp_dir = TempDir::new().unwrap();
405+
let home = temp_dir.path().join("state-root");
406+
let bin_dir = temp_dir.path().join("shared-bin");
407+
let cache_dir = temp_dir.path().join("cache-root");
408+
let venvs_dir = temp_dir.path().join("venv-root");
409+
let versions_dir = temp_dir.path().join("payload-root");
410+
411+
let layout = InstallLayout::from_parts(
412+
HostOs::Linux,
413+
home.clone(),
414+
bin_dir.clone(),
415+
cache_dir.clone(),
416+
venvs_dir.clone(),
417+
versions_dir.clone(),
418+
)
419+
.unwrap();
420+
421+
assert_eq!(layout.home(), home.as_path());
422+
assert_eq!(layout.bin_dir(), bin_dir);
423+
assert_eq!(layout.cache_dir(), cache_dir);
424+
assert_eq!(layout.venvs_dir(), venvs_dir);
425+
assert_eq!(layout.versions_dir(), versions_dir);
426+
}
320427
}

0 commit comments

Comments
 (0)