Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ after the language (`%LOCALAPPDATA%\Ruby`, like `%LocalAppData%\Python`)
rather than after the tool. rbmanager remains the product name.

```
rb setup [--yes] copy rb onto PATH and set up the VC++ runtime
rb install <zip|url> install a ruby binary package
rb list list installed rubies
rb use <version> switch the active ruby
rb uninstall <version> remove an installed ruby
rb enable [shell] print C++ build env to eval (cmd|powershell)
rb exec <command...> run a command with the C++ build env applied
rb setup [--yes] copy rb onto PATH and set up the VC++ runtime
rb install <zip|url> install a ruby binary package
rb list list installed rubies
rb use <version> switch the active ruby
rb uninstall <version> remove an installed ruby
rb msvc enable [shell] print the MSVC build env to eval (cmd|powershell)
rb msvc exec <command...> run a command with the MSVC build env applied
```

rb is a bare exe; `setup` copies it to
Expand All @@ -37,9 +37,9 @@ ship. It also checks for the VC++ 2015-2022 redistributable the
official mswin packages depend on, and offers to download and install
it (signature-verified, elevated); `--yes` skips the consent prompt.

`enable` and `exec` are the `ridk enable` equivalent for building
C extension gems with MSVC; see
[docs/devkit-enable.md](docs/devkit-enable.md).
`msvc enable` and `msvc exec` activate an installed Visual Studio (or
Build Tools) MSVC toolchain for building C extension gems; see
[docs/msvc-enable.md](docs/msvc-enable.md).

The active ruby is exposed through an NTFS directory junction
`%LOCALAPPDATA%\Ruby\current`, and `install` appends
Expand Down
80 changes: 40 additions & 40 deletions docs/devkit-enable.md → docs/msvc-enable.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# A `ridk enable` equivalent for the mswin packages
# Activating the MSVC build environment for the mswin packages

## Problem

Expand Down Expand Up @@ -35,14 +35,14 @@ Studio.
- `VsDevCmd.bat -arch=amd64 -host_arch=amd64` puts `cl`/`nmake`/`link`
on PATH and sets `INCLUDE`/`LIB`/`LIBPATH`/`VCToolsRedistDir`
(exit 0).
- Under the prototype `rb exec`, mkmf's `find_executable('cl')`
- Under the prototype `rb msvc exec`, mkmf's `find_executable('cl')`
succeeds, and a trivial C extension compiles, links, and loads:
`extconf.rb` -> `nmake` -> `require './hello.so'` returns a value from
native code.
- `rb enable powershell | Invoke-Expression` puts `cl` on the current
session's PATH (ridk parity).
- `rb msvc enable powershell | Invoke-Expression` puts `cl` on the
current session's PATH.

The conclusion is that a compiler-only `rb enable`/`rb exec` is fully
The conclusion is that a compiler-only `rb msvc enable`/`rb msvc exec` is fully
feasible and small. The interesting decisions are the command surface
and how far to go on third-party dependency headers.

Expand All @@ -53,40 +53,40 @@ environment. `ridk enable` only works because it is a shell function
whose output is eval'd into the current shell. Any activation feature
must work around this, and the two useful shapes are:

1. **`rb exec <command...>` (primary).** Spawns a child process with the
toolchain and active ruby already applied. No parent mutation, so
nothing to eval and nothing to get wrong. `rb exec gem install
nokogiri` just works. This is the recommended path for the common
case (one build command) and for scripts/CI, and it is the surface
that is bulletproof by construction.
1. **`rb msvc exec <command...>` (primary).** Spawns a child process
with the toolchain and active ruby already applied. No parent
mutation, so nothing to eval and nothing to get wrong. `rb msvc exec
gem install nokogiri` just works. This is the recommended path for
the common case (one build command) and for scripts/CI, and it is
the surface that is bulletproof by construction.

2. **`rb enable [cmd|powershell|pwsh]` (ridk parity).** Prints
2. **`rb msvc enable [cmd|powershell|pwsh]` (shell activation).** Prints
environment assignments for the user to eval into the current shell,
for interactive sessions where several build commands follow:

```
rem cmd
for /f "delims=" %L in ('rb enable cmd') do @%L
for /f "delims=" %L in ('rb msvc enable cmd') do @%L

# PowerShell / pwsh
rb enable powershell | Invoke-Expression
rb msvc enable powershell | Invoke-Expression
```

This mirrors `ridk enable` and is the escape hatch for users who want
a persistently activated shell rather than a per-command wrapper.
This is the escape hatch for users who want a persistently activated
shell rather than a per-command wrapper.

Recommend shipping both. `rb exec` is the headline; `rb enable` covers
the interactive workflow ridk users expect. A third option, writing a
Recommend shipping both. `rb msvc exec` is the headline; `rb msvc
enable` covers the interactive workflow. A third option, writing a
dot-sourced activation script into `%LOCALAPPDATA%\Ruby`, adds a file to
manage and a staleness problem (the resolved VS path is baked in) for no
gain over `rb enable`, so it is not recommended.
gain over `rb msvc enable`, so it is not recommended.

The parent-shell-mutation constraint is handled cleanly: `rb exec`
The parent-shell-mutation constraint is handled cleanly: `rb msvc exec`
sidesteps it entirely by owning the child's environment;
`rb enable` respects it by making the caller responsible for the eval,
exactly as ridk does.
`rb msvc enable` respects it by making the caller responsible for the
eval.

### Shell selection for `rb enable`
### Shell selection for `rb msvc enable`

The prototype takes the shell as an explicit argument and defaults to
PowerShell (the common interactive shell on modern Windows). Auto-
Expand Down Expand Up @@ -147,12 +147,12 @@ cmd /s /c "call "<installationPath>\Common7\Tools\VsDevCmd.bat" \
child inherits the calling shell's environment, so diffing the captured
`set` output against rb's own environment yields exactly the variables
VsDevCmd added or changed (PATH, INCLUDE, LIB, LIBPATH,
VCToolsRedistDir, and the VSCMD bookkeeping vars). `rb exec` applies
that delta to the child it spawns; `rb enable` prints it as `set
VCToolsRedistDir, and the VSCMD bookkeeping vars). `rb msvc exec`
applies that delta to the child it spawns; `rb msvc enable` prints it as `set
"K=V"` (cmd) or `$env:K = '...'` (PowerShell, single-quoted literal
with `'` doubled).

`rb exec` routes the user command through `cmd /s /c` so that `.cmd`
`rb msvc exec` routes the user command through `cmd /s /c` so that `.cmd`
shims (`gem`, `bundle`) and PATHEXT resolve the way they would if the
user had typed the command directly; a bare `CreateProcess` would not
find `gem` (it is `gem.cmd`).
Expand All @@ -166,8 +166,8 @@ name and the loader refuses to find it in the current directory, which
surfaces as the same cryptic "install development tools first" error
even though the compiler is present.

Both surfaces clear it for the activated environment: `rb exec` removes
the variable from the child's environment block, and `rb enable` emits
Both surfaces clear it for the activated environment: `rb msvc exec`
removes the variable from the child's environment block, and `rb msvc enable` emits
the unset (`set "NoDefault...="` for cmd, `Remove-Item Env:\NoDefault...`
for PowerShell). This is cheap insurance against a confusing failure and
is recommended.
Expand All @@ -194,7 +194,7 @@ and no opt-dir pointing at any such tree on the destination machine.

### Recommendation: phase 1 is compiler-only

Ship `rb exec`/`rb enable` as compiler-only first, and document the
Ship `rb msvc exec`/`rb msvc enable` as compiler-only first, and document the
dependency-linking limitation. This unblocks the large class of pure-C
gems immediately, is small and low-risk, and does not commit rbmanager
to shipping or versioning a pile of vcpkg dev files whose provenance and
Expand Down Expand Up @@ -241,20 +241,20 @@ should not depend on it.)

## Prototype

`src/rbmanager/Devkit.cs` implements both subcommands, wired into
`Program.cs`'s dispatch switch as `rb enable [shell]` and
`rb exec <command...>`. It is ~180 lines, marked as a prototype, and
`src/rbmanager/Msvc.cs` implements both subcommands, wired into
`Program.cs`'s dispatch switch as `rb msvc enable [shell]` and
`rb msvc exec <command...>`. It is ~180 lines, marked as a prototype, and
covers VS discovery, VsDevCmd activation with env-diffing, the
`NoDefaultCurrentDirectoryInExePath` clearing, and the per-shell output.
It is compiler-only (phase 1). What was exercised:

- `rb enable powershell|cmd` prints correct assignments; the PowerShell
form activates a live session via `| Invoke-Expression`.
- `rb exec ruby -rmkmf -e "find_executable('cl')"` finds the compiler.
- `rb exec` drives a full `extconf.rb` -> `nmake` -> load of a native
- `rb msvc enable powershell|cmd` prints correct assignments; the
PowerShell form activates a live session via `| Invoke-Expression`.
- `rb msvc exec ruby -rmkmf -e "find_executable('cl')"` finds the compiler.
- `rb msvc exec` drives a full `extconf.rb` -> `nmake` -> load of a native
extension.

A `gem install msgpack` under `rb exec` compiled several files (proving
A `gem install msgpack` under `rb msvc exec` compiled several files (proving
the toolchain is live) before failing on an
`RBIMPL_UNREACHABLE_RETURN`/`C2059` macro error in msgpack 1.8.3 against
Ruby 4.0's headers. That is an upstream gem/source incompatibility, not
Expand All @@ -263,14 +263,14 @@ appearing on the compile line.

## Open questions

1. Auto-detect the parent shell for `rb enable`, or keep the explicit
1. Auto-detect the parent shell for `rb msvc enable`, or keep the explicit
argument with a default? (Prototype: explicit, default PowerShell.)
2. Should `rb exec`/`rb enable` also guarantee the active ruby's
2. Should `rb msvc exec`/`rb msvc enable` also guarantee the active ruby's
`current\bin` is on PATH, or continue to rely on `install` having put
it there? (Prototype relies on install.)
3. Phase 2 trigger: is dependency-linking demand high enough to justify
bundling vcpkg dev files, and should the dev-file tree ship inside
the mswin package or as a separate rbmanager-managed download?
4. Should a leaked/nonexistent `--with-opt-dir` in `configure_args` be
actively stripped or overridden by `rb enable` even in phase 1, to
actively stripped or overridden by `rb msvc enable` even in phase 1, to
remove the confusing `-I<nonexistent>` from every compile line?
30 changes: 15 additions & 15 deletions docs/test-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ Command surface and contracts:
| `rb list` | Installed names sorted, active one starred | 0 |
| `rb use <query>` | Resolve query (exact or case-insensitive substring; must be unambiguous), recreate the `current` junction, ensure PATH | 0 / 1 |
| `rb uninstall <query>` | Resolve; if active, delete the junction first and print a hint; delete the install dir recursively | 0 / 1 |
| `rb enable [shell]` | Locate VsDevCmd via vswhere, compute the env delta of activation, print per-shell assignments plus an unset of `NoDefaultCurrentDirectoryInExePath`. Shell defaults to PowerShell; `cmd`/`bat` selects cmd syntax. No toolchain: actionable warning on stderr | 0 / 1 |
| `rb exec <cmd...>` | Same delta applied to a `cmd /s /c` child (so `.cmd` shims resolve); removes `NoDefaultCurrentDirectoryInExePath`; propagates the child's exit code | child / 1 |
| `rb msvc enable [shell]` | Locate VsDevCmd via vswhere, compute the env delta of activation, print per-shell assignments plus an unset of `NoDefaultCurrentDirectoryInExePath`. Shell defaults to PowerShell; `cmd`/`bat` selects cmd syntax. No toolchain: actionable warning on stderr | 0 / 1 |
| `rb msvc exec <cmd...>` | Same delta applied to a `cmd /s /c` child (so `.cmd` shims resolve); removes `NoDefaultCurrentDirectoryInExePath`; propagates the child's exit code | child / 1 |
| anything else | Usage text | 2 |

Any thrown exception is caught in `Main`, printed as `rb: <message>` to
Expand Down Expand Up @@ -63,7 +63,7 @@ stderr, exit 1.
and deletes it in `Dispose`. Junction targets and junction points
both live inside it.
- Tests that redirect `Console.Out`/`Console.Error` or mutate the
process environment (`Program`/`Devkit` in-process tests) go in one
process environment (`Program`/`Msvc` in-process tests) go in one
xUnit collection (`[Collection("process-global")]`) so they never
run in parallel with each other. E2E tests spawn processes and can
stay parallel because each gets its own root via the env seam.
Expand Down Expand Up @@ -97,7 +97,7 @@ Keep this to the minimum below; each item is a mechanical change.
an env var `RBMANAGER_ENV_KEY` naming an alternative HKCU-relative
subkey (and suppress the broadcast when it is set) so a full
`rb install` run never touches the real PATH.
3. `Devkit` seams. Make `VsWhere` an internal settable property (env
3. `Msvc` seams. Make `VsWhere` an internal settable property (env
override `RBMANAGER_VSWHERE` for E2E), and make `ActivatedDelta`,
`LocateVsDevCmd`, `ParseShell`, `Assignment`, `Unset`, `QuoteArg`
internal instead of private. `ActivatedDelta(vsdevcmd)` already takes
Expand Down Expand Up @@ -208,9 +208,9 @@ Drive the exe built by `dotnet build` (see section 5 for AOT).

34. No args → usage on stdout, exit 2.
35. Unknown command → usage, exit 2.
36. `install` with no argument, `use` with no argument, `exec` with no
command → usage, exit 2 (the `exec` pattern requires a non-empty
command).
36. `install` with no argument, `use` with no argument, `msvc` with no
subcommand, `msvc exec` with no command → usage, exit 2 (the
`msvc exec` pattern requires a non-empty command).
37. Failing command (e.g. `use nosuch`) → stderr starts with `rb: `,
exit 1, stdout empty.
38. Full lifecycle: install A → list (A starred) → install B → list (B
Expand Down Expand Up @@ -265,7 +265,7 @@ All against `HKCU\Software\rbmanager-tests\<guid>` with
55. Empty-string existing value → result is exactly the entry, no
leading `;`.

### 4.8 Devkit: pure helpers — Unit
### 4.8 Msvc: pure helpers — Unit

56. `ParseShell`: `null`, `powershell`, `pwsh`, `ps` → PowerShell;
`cmd`, `bat` → Cmd; `zsh` → throws `unknown shell 'zsh'`. (Note
Expand All @@ -280,7 +280,7 @@ All against `HKCU\Software\rbmanager-tests\<guid>` with
quotes; empty string → `""`; tab → quoted; embedded `"` → not
escaped (pin as known limitation; see 6.7).

### 4.9 Devkit: activation with a stub VsDevCmd — Integration
### 4.9 Msvc: activation with a stub VsDevCmd — Integration

Stub `.bat` fixture written per test, e.g. sets `RB_TEST_NEW=hello`,
modifies `PATH` by prefixing a marker dir, sets a var whose value
Expand All @@ -300,29 +300,29 @@ contains `=` and one containing non-ASCII, and `exit /b 0`.
66. `Enable`/`Exec` with the toolchain seam pointing nowhere and
`VsWhere` set to a nonexistent path → stderr warning containing the
winget hint, exit 1, stdout empty (the warning must not go to
stdout, since `rb enable | Invoke-Expression` would eval it).
stdout, since `rb msvc enable | Invoke-Expression` would eval it).
67. `LocateVsDevCmd` with `VsWhere` nonexistent → null (covered
behaviorally by 66; also assert directly).
68. `Exec` with stub: run `cmd /c set` as the command, capture output →
child sees the stub's variables and does not see
`NoDefaultCurrentDirectoryInExePath` (set it in the test process
first).
69. `Exec` exit-code propagation: `rb exec cmd /c exit 7` → 7.
69. `Exec` exit-code propagation: `rb msvc exec cmd /c exit 7` → 7.
70. `Exec` resolves `.cmd` shims: put a `hello.cmd` on the stub-added
PATH dir, `exec hello` → runs it (proves the `cmd /s /c` routing
PATH dir, `msvc exec hello` → runs it (proves the `cmd /s /c` routing
and PATHEXT behavior).
71. `Exec` argument quoting: an argument with spaces survives to the
child (child echoes `%1`-style or a tiny script writes its argv to
a file).

### 4.10 Devkit against real Visual Studio — RequiresVS (opt-in)
### 4.10 Msvc against real Visual Studio — RequiresVS (opt-in)

Skipped unless vswhere resolves an install (use a runtime skip, e.g.
`Assert.Skip`/`SkippableFact`).

72. `LocateVsDevCmd` returns an existing `VsDevCmd.bat`.
73. `ActivatedDelta` includes `INCLUDE`, `LIB`, and a `PATH` change.
74. `rb exec cl` (E2E) exits 0 with cl's banner on stderr.
74. `rb msvc exec cl` (E2E) exits 0 with cl's banner on stderr.

### 4.11 AOT publish smoke — E2E (opt-in, slow)

Expand Down Expand Up @@ -390,6 +390,6 @@ a comment. Each is a product decision to make separately.
5. Dangling `current` (target deleted out of band) has unpinned
semantics in `CurrentTarget`/`Uninstall` (case 30 pins it).
6. `ParseShell` is case-sensitive (`PowerShell` is rejected).
7. `QuoteArg` does not escape embedded quotes; `rb exec` with an
7. `QuoteArg` does not escape embedded quotes; `rb msvc exec` with an
argument containing `"` produces a broken cmd line (case 60 pins
the helper's output only).
28 changes: 15 additions & 13 deletions src/rbmanager/Devkit.cs → src/rbmanager/Msvc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,24 @@

namespace RbManager;

// PROTOTYPE: a `ridk enable` equivalent for the mswin packages.
// PROTOTYPE: MSVC build-environment activation for the mswin packages.
//
// The official mswin binary carries no devkit, so `gem install <native>`
// has no compiler on PATH and fails with mkmf's cryptic "install
// The official mswin binary ships no compiler, so `gem install <native>`
// has no toolchain on PATH and fails with mkmf's cryptic "install
// development tools first". This locates an installed Visual Studio (or
// Build Tools) C++ toolchain, activates it the same way ruby/actions'
// mswin-build workflow does (VsDevCmd.bat), and exposes that environment
// two ways:
// Build Tools) MSVC toolchain, activates it the same way ruby/actions'
// mswin-build workflow does (VsDevCmd.bat, the script behind Visual
// Studio's Developer Command Prompt), and exposes that environment two
// ways:
//
// rb enable [cmd|powershell|pwsh] print env assignments to eval in the
// current shell (ridk-parity)
// rb exec -- <command...> run one command with the toolchain
// already applied (no shell mutation)
// rb msvc enable [cmd|powershell|pwsh] print env assignments to eval
// in the current shell
// rb msvc exec <command...> run one command with the
// toolchain already applied
// (no shell mutation)
//
// See docs/devkit-enable.md for the design rationale.
internal static class Devkit
// See docs/msvc-enable.md for the design rationale.
internal static class Msvc
{
// vswhere ships at a fixed, versionless path with the VS Installer and
// is the only supported way to locate installs (including Build-Tools-
Expand Down Expand Up @@ -154,7 +156,7 @@ public static int Exec(string[] command)

// Fails fast with the setup steps instead of letting mkmf die later
// with its cryptic "install development tools first". stderr only, so
// an eval'd `rb enable` pipeline never swallows it.
// an eval'd `rb msvc enable` pipeline never swallows it.
private static int WarnMissingToolchain()
{
Console.Error.WriteLine("""
Expand Down
Loading
Loading