From 0453a9c9952534990729b1a74d1e3bf5134276ea Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Thu, 5 Nov 2026 19:55:55 +0100 Subject: [PATCH 1/3] feat(installer): offer local project installation --- INSTALLER_PLAN.md | 4 +++ README.md | 19 ++++++++----- SPEC.md | 43 ++++++++++++++++++++++------- tests/README.md | 34 +++++++++++++++++++++++ tests/tool-installer.ps1 | 58 ++++++++++++++++++++++++++++++++++++++++ tests/tool-installer.sh | 32 ++++++++++++++++++++++ web/index.html | 2 +- web/install.ps1 | 31 ++++++++++++++++++--- web/install.sh | 35 ++++++++++++++++++++---- 9 files changed, 232 insertions(+), 26 deletions(-) diff --git a/INSTALLER_PLAN.md b/INSTALLER_PLAN.md index 8040e32..fab0297 100644 --- a/INSTALLER_PLAN.md +++ b/INSTALLER_PLAN.md @@ -6,6 +6,10 @@ publication and hosted verification remain maintainer steps in RELEASING.md. SPEC.md records current behavior. The user simplified installation on 2026-11-05 to temporary extraction and direct copying, removing ownership, locking, history, recovery and permanent backups. +On the same date, the user added a first choice between local project copying +and the existing user installation. Local mode copies all three scripts into +the working directory and skips environment setup; see SPEC.md for current +interfaces. The user-install work breakdown below predates this addition. Read `AGENTS.md` and `SPEC.md` before implementation. This document defines the work breakdown; `SPEC.md` remains the product contract. When implementing, update the contract to distinguish completed behavior from remaining proposals. diff --git a/README.md b/README.md index e22d895..f85396f 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,8 @@ sure it's the correct version! ## Using it installed -Install `jvm` for your user account in `~/.local/bin`: +Run the installer to choose a local project copy or installation for your user +account in `~/.local/bin`: ```sh # Linux, macOS, Git Bash, MSYS2 or Cygwin: @@ -107,20 +108,26 @@ irm 'https://getjvm.dev/install.ps1' -OutFile "$env:TEMP\jvm-install.ps1" & "$env:TEMP\jvm-install.ps1" -Version 0.2.0 -Path -NoJava -NonInteractive ``` -The installer asks separately whether to add `jvm` to PATH and whether to make -its default Java available in your terminals. -After setup, it checks jvm's inventory and suggests `jvm install` if no usable -JDK is installed. +The first question offers two choices: + +1. **Install locally:** copy all three scripts into the current directory, + without changing your environment. Useful for including `jvm` in a Java project. +2. **Install for your user:** install in `~/.local/bin`, then optionally add + `jvm` to PATH and make its default Java available in your terminals. + You can also choose explicitly: | Setup | Bash | PowerShell | | --- | --- | --- | +| Install in the current directory | `--local` | `-Local` | +| Install for your user | `--user` | `-User` | | Add jvm to PATH | `--path` | `-Path` | | Set up default Java | `--java` | `-Java` | | Skip either setup | `--no-path`, `--no-java` | `-NoPath`, `-NoJava` | | Skip prompts | `--non-interactive` | `-NonInteractive` | -Without prompts, setup is skipped unless explicitly requested. The Bash +Without prompts, user installation is the default and setup is skipped unless +explicitly requested. Local mode always skips environment setup. The Bash installer configures your selected shell, including on Windows. Use the PowerShell installer to configure your Windows user environment. diff --git a/SPEC.md b/SPEC.md index a97a33f..1b98bea 100644 --- a/SPEC.md +++ b/SPEC.md @@ -650,20 +650,22 @@ external runtime assets or dependencies. The local `web/duke-claw.png` artwork appears above the main heading in the left column; the header uses a text-only wordmark. -`web/install.sh` and `web/install.ps1` implement standalone user-wide tool +`web/install.sh` and `web/install.ps1` implement standalone local or user-wide tool installation; public URLs require the maintainer publication sequence in [RELEASING.md](RELEASING.md). They install no JDK, never select a default, never -modify project copies and introduce no `use` command. The defaults proposed in +modify project copies during user installation and introduce no `use` command. +The defaults proposed in [INSTALLER_PLAN.md](INSTALLER_PLAN.md) were approved for this implementation. ```text install.sh [--version major.minor.patch] + [--local | --user] [--path | --no-path] [--java | --no-java] [--shell bash|zsh|fish|csh|tcsh] [--profile /absolute/file] [--non-interactive] install.sh --help install.ps1 [-Version major.minor.patch] [-Path | -NoPath] - [-Java | -NoJava] [-NonInteractive] + [-Local | -User] [-Java | -NoJava] [-NonInteractive] install.ps1 -Help ``` @@ -671,6 +673,18 @@ Options are unique; conflicting/unknown options and invalid versions exit 2 before downloads or directory creation. Profile requires an explicit shell. Shell/profile options apply to all Bash hosts; PowerShell is Windows-only. Help is offline. + +Before downloading, interactive installation first asks where to install: +`1. Install locally` copies all three scripts into the current working directory +for inclusion in a Java project, without changing the environment; +`2. Install for your user` installs into HOME/.local/bin with the existing +optional PATH and JAVA_HOME setup. Enter selects option 2; invalid answers retry. +`--local`/`-Local` and `--user`/`-User` select a mode without this question. +Without a usable terminal, in noninteractive mode or on failed input, the +unspecified mode remains user installation. Bash reads this question from +`/dev/tty`, never bootstrap stdin. Local mode ignores environment/shell/profile +setup choices after validating their syntax and asks no further questions. + Versions have exactly three numeric dotted components, each `0` or a positive integer without leading zeros. Exit 0 means the tool and any requested setup succeeded (optional setup may be declined); operational failure exits 1. If @@ -711,14 +725,18 @@ Downloads use the standard redirect handling of curl, wget or .NET WebClient. Curl restricts protocols to HTTPS; wget and WebClient use their built-in redirect policies. There is no custom redirect parser. PowerShell enables TLS 1.2. -The destination is resolved HOME/.local/bin, independent of XDG_DATA_HOME. +The user-install destination is resolved HOME/.local/bin, independent of +XDG_DATA_HOME. +Local installation instead uses the current working directory and copies all +three exact-byte scripts on every supported host; Bash makes `jvm` executable. +Both modes retain the same HOME/data-root validation and shared JDK inventory. Bash requires absolute HOME; PowerShell uses HOME, falling back to USERPROFILE only when HOME is absent, and requires a native absolute path. Relative home values are errors. Data storage follows the existing absolute XDG override and fallback policy. Newline, CR, tab and PATH-separator paths are rejected before mutation (Bash also rejects semicolons for Windows interoperability). Bash -ships only `jvm` with executable permissions on Linux and macOS. On Windows, -both installers ship all three exact-byte scripts. On Git Bash, MSYS2 and +user installation ships only `jvm` with executable permissions on Linux and +macOS. On Windows, both installers ship all three exact-byte scripts. On Git Bash, MSYS2 and Cygwin, `install.sh` uses the same direct file copying and shell-profile setup as on Unix. It requires `cygpath` for native HOME/data-root inputs and emits native forward-slash @@ -731,7 +749,7 @@ from PowerShell separately. No PowerShell source is embedded in `install.sh`. Download and extract in a unique temporary directory under `${TMPDIR:-/tmp}` for Bash or the Windows temporary directory (`%TEMP%`) for PowerShell. Create -HOME/.local/bin and copy the required scripts directly there, overwriting +the destination and copy the required scripts directly there, overwriting existing copies. Bash makes `jvm` executable. Each invocation downloads and copies again; there is no installed-version check, ownership record, per-file hash inventory, lock, destination-side staging, history or recovery journal. @@ -751,8 +769,8 @@ Abrupt termination does not trigger recovery on a subsequent invocation. ### Persistent environment setup -Tool PATH and default-Java setup have independent consent. Positive options -consent to the corresponding changes; negative options suppress them. Otherwise +User installation's tool PATH and default-Java setup have independent consent. +Positive options consent to the corresponding changes; negative options suppress them. Otherwise show a shared introduction listing the shell startup files or user environment variables (HKCU\Environment), then explain each choice in terms of its effect in future terminals. Show the added paths and current/new JAVA_HOME. Both @@ -768,7 +786,12 @@ downloaded scripts. Count safe ordinary installation directories with usable java/javac launchers, including installations without manifests; exclude links, reserved reference names and invalid layouts. If none exist, print `No JDK is installed yet. Install one with:` followed by `jvm install`. This check never -creates JDK storage or selects/installs a JDK. End with `Setup complete.` and +creates JDK storage or selects/installs a JDK. Local mode performs the same +inventory check, but suggests `./jvm install` in Bash and `.\jvm install` in +PowerShell. If a usable JDK is found, print `See your currently installed JDKs +with:` followed by `jvm list` for user installation, `./jvm list` for local +Bash installation, or `.\jvm list` for local PowerShell installation. +End with `Setup complete.` and show the new-terminal reminder only when environment settings changed. An already-correct managed setup avoids repeated prompts and edits. The Bash installer compares managed setup text using Bash itself; it diff --git a/tests/README.md b/tests/README.md index 83d5446..c1302a8 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,5 +1,39 @@ # Discovery, installation and uninstallation verification +## Installed-JDK reminder (2026-11-05) + +Both installers now suggest listing the installed JDKs when the managed +inventory contains a usable installation. Regression assertions cover +`jvm list` for user installation, `./jvm list` for local Bash installation +and `.\jvm list` for local PowerShell installation, while retaining install +advice for empty inventories. The checks use the existing isolated offline +fixtures and never execute JDK launchers. + +Verification passed all 19 offline Git Bash installer groups (simulated hosts), +all 14 native Windows PowerShell 5.1 installer groups, Bash syntax checks and +`git diff --check`. No native Unix/Cygwin/MSYS2 or Bash 3.2 runs were performed; +ShellCheck remains unavailable. The installers retain LF endings and measure +19,133 bytes (`web/install.sh`) and 16,660 bytes (`web/install.ps1`). + +## Local tool installation (2026-11-05) + +Both installers first offer local project copying or the existing user install. +Local mode copies all three exact-byte scripts, skips environment setup even +when requested by flags, and gives shell-appropriate relative JDK installation +advice. Offline regressions exercise the question with fixture input, explicit +local installation/reinstallation, conflicting mode options, spaced working +directories, preservation after download failure, and absence of user/profile, +registry and JDK-storage changes. + +Installed Git Bash passed all 19 offline installer groups with simulated host +detection. Windows PowerShell 5.1 passed all 14 installer groups using temporary HOME/XDG +roots and a disposable registry key. Bash and installer syntax checks, +PowerShell parsing and `git diff --check` passed. ShellCheck is unavailable. +The prompt tests simulate input; no live interactive terminal session, native +Unix/Cygwin/MSYS2 or Bash 3.2 validation was performed for this change. +Installer sizes: 19,044 bytes (`web/install.sh`) and 16,559 bytes +(`web/install.ps1`), both LF. + ## Installer setup wording (2026-11-05) Both installers list environment targets once, explain each optional setup in diff --git a/tests/tool-installer.ps1 b/tests/tool-installer.ps1 index 35b9f52..9b92ed5 100644 --- a/tests/tool-installer.ps1 +++ b/tests/tool-installer.ps1 @@ -54,6 +54,8 @@ try { } Invoke-JvmInstaller @('-Help') | Out-Null Expect-Failure { Invoke-JvmInstaller @('-Path', '-NoPath') } 2 + Expect-Failure { Invoke-JvmInstaller @('-Local', '-User') } 2 + Expect-Failure { Invoke-JvmInstaller @('-Local', '-Local') } 2 Expect-Failure { Invoke-JvmInstaller @('-Version', '01.2.3') } 2 Expect-Failure { Invoke-JvmInstaller @('-Shell', 'bash') } 2 Assert ($script:requests.Count -eq 0 -and -not (Test-Path -LiteralPath $env:HOME)) 'help/usage caused side effects' @@ -65,6 +67,49 @@ try { & $env:ComSpec /d /c $publicInvalid Assert ($LASTEXITCODE -eq 2) 'CMD public invalid argument exit status' Pass 'public PowerShell/CMD entry points and exit statuses' + $originalMode = ${function:Get-InstallationMode} + try { + # Replace only terminal detection; input still passes through the real question. + ${function:Get-InstallationMode} = [scriptblock]::Create($originalMode.ToString().Replace('$Unattended -or [Console]::IsInputRedirected -or -not [Environment]::UserInteractive', '$Unattended')) + $script:answers = New-Object 'Collections.Generic.Queue[string]' + function Read-Host { return $script:answers.Dequeue() } + $script:answers.Enqueue('invalid'); $script:answers.Enqueue('1') + Assert ((Get-InstallationMode '' $false) -eq 'local') 'first question did not select local' + $script:answers.Enqueue('2') + Assert ((Get-InstallationMode '' $false) -eq 'user') 'first question did not select user' + $script:answers.Enqueue('') + Assert ((Get-InstallationMode '' $false) -eq 'user') 'Enter did not preserve user default' + Assert ((Get-InstallationMode '' $true) -eq 'user') 'unattended default changed' + } finally { + ${function:Get-InstallationMode} = $originalMode + Remove-Item Function:Read-Host + } + Pass 'first question accepts local/user choices, retries invalid answers and preserves user default' + $project = Join-Path $temp 'project with spaces' + [IO.Directory]::CreateDirectory($project) | Out-Null + Push-Location -LiteralPath $project + try { + $savedError = [Console]::Error + $localMessages = New-Object IO.StringWriter + try { + [Console]::SetError($localMessages) + Invoke-JvmInstaller @('-Local', '-Path', '-Java', '-NonInteractive') + } finally { [Console]::SetError($savedError) } + Assert ($localMessages.ToString().Contains(' .\jvm install')) 'missing relative PowerShell install advice' + Assert (-not $localMessages.ToString().Contains('future terminals')) 'local install offered environment setup' + $localMessages.Dispose() + foreach ($name in @('jvm', 'jvm.ps1', 'jvm.cmd')) { Assert ((Get-ToolHash (Join-Path $project $name)) -ceq $hashes[$name]) "local payload differs: $name" } + Assert (-not (Test-Path -LiteralPath $env:HOME)) 'local installation created user destination' + Assert (-not (Test-Path -LiteralPath $env:XDG_DATA_HOME)) 'local installation created JDK storage' + Assert (-not [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($registryPath)) 'local installation accessed environment setup' + Invoke-JvmInstaller @('-Local', '-NonInteractive') + $script:downloadFailure = $true + Expect-Failure { Invoke-JvmInstaller @('-Local', '-NonInteractive') } + $script:downloadFailure = $false + foreach ($name in @('jvm', 'jvm.ps1', 'jvm.cmd')) { Assert ((Get-ToolHash (Join-Path $project $name)) -ceq $hashes[$name]) "local failure changed: $name" } + } finally { Pop-Location } + Pass 'local install/reinstall copies three scripts without user destination, storage or registry setup' + $script:requests = @() Invoke-JvmInstaller @('-NonInteractive', '-NoPath', '-NoJava') $destination = Join-Path $env:HOME '.local\bin' foreach ($name in @('jvm', 'jvm.ps1', 'jvm.cmd')) { Assert ((Get-ToolHash (Join-Path $destination $name)) -ceq $hashes[$name]) "payload differs: $name" } @@ -85,6 +130,19 @@ try { Assert (-not (Test-InstalledJdk $env:XDG_DATA_HOME)) 'missing javac counted as installed' [IO.File]::WriteAllText((Join-Path $jdk 'javac.cmd'), 'must not execute') Assert (Test-InstalledJdk $env:XDG_DATA_HOME) 'manifest-free installation was missed' + foreach ($modeOption in @('-User', '-Local')) { + Push-Location -LiteralPath $project + $savedError = [Console]::Error + $messages = New-Object IO.StringWriter + try { + [Console]::SetError($messages) + Invoke-JvmInstaller @($modeOption, '-NonInteractive', '-NoPath', '-NoJava') + $expectedCommand = ' jvm list' + if ($modeOption -eq '-Local') { $expectedCommand = ' .\jvm list' } + Assert ($messages.ToString().Contains($expectedCommand)) "missing installed-JDK advice for $modeOption" + Assert (-not $messages.ToString().Contains('No JDK is installed yet')) 'installed JDK prompted installation' + } finally { [Console]::SetError($savedError); $messages.Dispose(); Pop-Location } + } Pass 'post-setup inventory excludes reserved/invalid layouts and accepts native launchers without execution' Invoke-JvmInstaller @('-NonInteractive') diff --git a/tests/tool-installer.sh b/tests/tool-installer.sh index 933c2be..f57eeaf 100644 --- a/tests/tool-installer.sh +++ b/tests/tool-installer.sh @@ -77,11 +77,36 @@ fails() { } run --help fails 2 --path --no-path +fails 2 --local --user +fails 2 --local --local fails 2 --version 01.2.3 fails 2 --version "$(printf '0.2.0\ninvalid')" fails 2 --profile /tmp/x [ ! -e "$LOG" ] && [ ! -e "$HOME/.local" ] ok 'help and invalid options have no side effects' +# Exercise the first question with fixture input instead of a controlling terminal. +sed -e 's|> /dev/tty|>\&2|g' -e 's|< /dev/tty|< "$TEST_INSTALLER_ANSWERS"|g' -e 's|} 2>/dev/null|}|g' "$INSTALLER" > "$TEMP/prompt-installer" +export TEST_INSTALLER_ANSWERS="$TEMP/answers" +mkdir -p "$TEMP/project with spaces" +printf '1\n' > "$TEMP/answers" +( + cd "$TEMP/project with spaces" + bash "$TEMP/prompt-installer" --path --java > "$TEMP/out" 2> "$TEMP/err" +) +for name in jvm jvm.ps1 jvm.cmd; do cmp "$TEMP/project with spaces/$name" "$ROOT/$name"; done +grep -q 'How would you like to install jvm?' "$TEMP/err" +grep -q '^ ./jvm install$' "$TEMP/err" +! grep -q 'Enable this?\|future terminals\|Open a new terminal' "$TEMP/err" +[ ! -e "$HOME/.local" ] && [ ! -e "$HOME/.bashrc" ] && [ ! -e "$XDG_DATA_HOME" ] +# Reinstall explicitly without prompting, and preserve copies after failed verification. +( + cd "$TEMP/project with spaces" + run --local --non-interactive + DOWNLOAD_FAIL=1 fails 1 --local --non-interactive +) +for name in jvm jvm.ps1 jvm.cmd; do cmp "$TEMP/project with spaces/$name" "$ROOT/$name"; done +ok 'first-question local choice and explicit local reinstall copy three scripts without setup' +: > "$LOG" run --non-interactive --no-path --no-java cmp "$DEST/jvm" "$ROOT/jvm" [ ! -e "$HOME/.bashrc" ] && [ ! -e "$XDG_DATA_HOME/jvm/jdks" ] @@ -100,6 +125,13 @@ mkdir -p "$XDG_DATA_HOME/jvm/jdks/.custom JDK/bin" for name in java javac; do cp "$XDG_DATA_HOME/jvm/jdks/default/bin/$name" "$XDG_DATA_HOME/jvm/jdks/.custom JDK/bin/$name"; done run --non-interactive --no-path --no-java ! grep -q 'No JDK is installed yet' "$TEMP/err" +grep -q '^ jvm list$' "$TEMP/err" +( + cd "$TEMP/project with spaces" + run --local --non-interactive + grep -q '^ ./jvm list$' "$TEMP/err" + ! grep -q 'No JDK is installed yet' "$TEMP/err" +) # Leave reserved and broken directories in place; exclude a linked installation. mv "$XDG_DATA_HOME/jvm/jdks/.custom JDK" "$TEMP/external-jdk" ln -s "$TEMP/external-jdk" "$XDG_DATA_HOME/jvm/jdks/linked" diff --git a/web/index.html b/web/index.html index f127cf6..f9b81e4 100644 --- a/web/index.html +++ b/web/index.html @@ -127,7 +127,7 @@

Windows — PowerShell

iex (irm 'https://getjvm.dev/install.ps1')
-

02 / Make yourself at home.
Open a fresh terminal application, then install Java:

+

02 / Make yourself at home.
After user environment setup, open a fresh terminal application, then install Java. For a local copy, stay in the same directory and use ./jvm install (PowerShell: .\jvm install).

jvm install
diff --git a/web/install.ps1 b/web/install.ps1 index 063d141..4835b10 100644 --- a/web/install.ps1 +++ b/web/install.ps1 @@ -94,6 +94,20 @@ namespace JvmInstaller { public static class Native { [JvmInstaller.Native]::SendMessageTimeout([IntPtr]0xffff, 0x1a, [UIntPtr]::Zero, 'Environment', 2, 5000, [ref]$result) | Out-Null } function Test-SetupAnswer([string]$Answer) { return $Answer -match '\A(?i:|y|yes)\z' } +function Get-InstallationMode([string]$Mode, [bool]$Unattended) { + if ($Mode) { return $Mode } + if ($Unattended -or [Console]::IsInputRedirected -or -not [Environment]::UserInteractive) { return 'user' } + Write-InstallerMessage "`nHow would you like to install jvm?`n`n1. Install locally`n Copy all three jvm scripts to the current directory, without changing your`n environment. Useful for including jvm in a Java project.`n`n2. Install for your user`n Install jvm in HOME/.local/bin, with optional PATH and JAVA_HOME setup.`n Useful for keeping jvm available across projects.`n" + while ($true) { + try { $answer = Read-Host 'Choose [1/2] (default: 2)' } catch { return 'user' } + switch -Exact ($answer) { + '1' { return 'local' } + '2' { return 'user' } + '' { return 'user' } + default { Write-InstallerMessage 'Please enter 1 or 2.' } + } + } +} function Get-SetupConsent([string]$Choice, [bool]$Unattended, [string]$Prompt) { if ($Choice -eq 'yes') { return $true } if ($Choice -eq 'no' -or $Unattended -or [Console]::IsInputRedirected -or -not [Environment]::UserInteractive) { return $false } @@ -179,18 +193,20 @@ function Test-InstalledJdk([string]$Data) { } function Invoke-JvmInstaller([string[]]$Arguments) { $script:EnvironmentChanged = $false - $version = ''; $pathChoice = 'ask'; $javaChoice = 'ask'; $unattended = $false + $version = ''; $pathChoice = 'ask'; $javaChoice = 'ask'; $unattended = $false; $mode = '' $seen = @{} for ($i = 0; $i -lt $Arguments.Count; $i++) { $arg = $Arguments[$i] if ($arg -ieq '-Help') { if ($Arguments.Count -ne 1) { Stop-Installer '-Help must be used alone' 2 } - Write-Output 'Usage: install.ps1 [-Version major.minor.patch] [-Path | -NoPath] [-Java | -NoJava] [-NonInteractive]' + Write-Output 'Usage: install.ps1 [-Version major.minor.patch] [-Local | -User] [-Path | -NoPath] [-Java | -NoJava] [-NonInteractive]' + Write-Output 'Local installation copies all three scripts to the current directory, without setup. Without a terminal, user installation remains the default.' return } $group = $arg.ToLowerInvariant() if ($group -in @('-path', '-nopath')) { $group = 'path' } if ($group -in @('-java', '-nojava')) { $group = 'java' } + if ($group -in @('-local', '-user')) { $group = 'mode' } if ($seen.ContainsKey($group)) { Stop-Installer "Duplicate/conflicting option: $arg" 2 } $seen[$group] = $true switch ($arg.ToLowerInvariant()) { @@ -199,10 +215,13 @@ function Invoke-JvmInstaller([string[]]$Arguments) { '-nopath' { $pathChoice = 'no' } '-java' { $javaChoice = 'yes' } '-nojava' { $javaChoice = 'no' } + '-local' { $mode = 'local' } + '-user' { $mode = 'user' } '-noninteractive' { $unattended = $true } default { Stop-Installer "Unknown option: $arg" 2 } } } + $mode = Get-InstallationMode $mode $unattended if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) { Stop-Installer 'The native installer supports Windows only' } $tar = Join-Path ([Environment]::SystemDirectory) 'tar.exe' if (-not [IO.File]::Exists($tar)) { @@ -219,6 +238,7 @@ function Invoke-JvmInstaller([string[]]$Arguments) { Assert-SafeRoot $data $data = [IO.Path]::GetFullPath($data) $destination = Join-Path $userHome '.local\bin' + if ($mode -eq 'local') { $destination = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath('.') } $javaHome = Join-Path $data 'jvm\jdks\default' $work = Join-Path ([IO.Path]::GetTempPath()) ('jvm-bootstrap-' + [Guid]::NewGuid().ToString('N')) [IO.Directory]::CreateDirectory($work) | Out-Null @@ -240,9 +260,12 @@ function Invoke-JvmInstaller([string[]]$Arguments) { } $installed = $true Write-InstallerMessage "jvm $version installed successfully in $destination." - Set-UserEnvironment $destination $javaHome $pathChoice $javaChoice $unattended + if ($mode -eq 'user') { Set-UserEnvironment $destination $javaHome $pathChoice $javaChoice $unattended } try { - if (-not (Test-InstalledJdk $data)) { Write-InstallerMessage "`nNo JDK is installed yet. Install one with:`n jvm install" } + $invocation = 'jvm' + if ($mode -eq 'local') { $invocation = '.\jvm' } + if (Test-InstalledJdk $data) { Write-InstallerMessage "`nSee your currently installed JDKs with:`n $invocation list" } + else { Write-InstallerMessage "`nNo JDK is installed yet. Install one with:`n $invocation install" } } catch { Write-InstallerMessage "Could not check installed JDKs: $($_.Exception.Message)" } Write-InstallerMessage 'Setup complete.' if ($script:EnvironmentChanged) { Write-InstallerMessage 'Open a new terminal application to use your changes.' } diff --git a/web/install.sh b/web/install.sh index 721f3ca..6080b42 100644 --- a/web/install.sh +++ b/web/install.sh @@ -24,7 +24,7 @@ # Standalone Bash bootstrap; persistent setup uses shell profiles on every host. set -eu VERSION='' PATH_CHOICE='' JAVA_CHOICE='' SHELL_CHOICE='' PROFILE='' NONINTERACTIVE=0 -WORK='' INSTALLED=0 DEST='' DATA='' ENV_CHANGED=0 +WORK='' INSTALLED=0 DEST='' DATA='' ENV_CHANGED=0 MODE='' FILES=(jvm) SETUP_FILES=() SETUP_BACKUPS=() SETUP_EXISTED=() say() { printf '%s\n' "jvm installer: $*" >&2; } @@ -32,13 +32,16 @@ die() { say "$*"; exit 1; } usage_error() { say "$*"; exit 2; } help_text() { cat <<'EOF' -Install or update jvm in HOME/.local/bin (no JDK is installed). +Install or update jvm locally or in HOME/.local/bin (no JDK is installed). Usage: install.sh [--version major.minor.patch] + [--local | --user] [--path | --no-path] [--java | --no-java] [--shell bash|zsh|fish|csh|tcsh] [--profile /absolute/file] [--non-interactive] Explicit positive options consent to persistent environment edits. Otherwise separate prompts default to yes; unattended setup is skipped. +Local installation copies all three scripts to the current directory, without setup. +Without a terminal, user installation remains the default. EOF } valid_version() { @@ -57,6 +60,7 @@ parse() { --profile) [ -z "$PROFILE" ] || usage_error 'duplicate profile'; PROFILE=$2 ;; esac; shift ;; --path|--no-path) [ -z "$PATH_CHOICE" ] || usage_error 'conflicting/duplicate PATH options'; PATH_CHOICE=$1 ;; + --local|--user) [ -z "$MODE" ] || usage_error 'conflicting/duplicate installation options'; MODE=${1#--} ;; --java|--no-java) [ -z "$JAVA_CHOICE" ] || usage_error 'conflicting/duplicate Java options'; JAVA_CHOICE=$1 ;; --non-interactive) [ "$NONINTERACTIVE" = 0 ] || usage_error 'duplicate non-interactive'; NONINTERACTIVE=1 ;; *) usage_error "unknown option: $1" ;; @@ -128,6 +132,18 @@ quote() { setup_answer_yes() { case $1 in ''|y|Y|[yY][eE][sS]) return 0 ;; *) return 1 ;; esac } +choose_installation() { + [ -z "$MODE" ] || return 0 + MODE=user + [ "$NONINTERACTIVE" = 0 ] && [ -r /dev/tty ] && [ -w /dev/tty ] || return 0 + local answer + while :; do + { printf '\nHow would you like to install jvm?\n\n1. Install locally\n Copy all three jvm scripts to the current directory, without changing your\n environment. Useful for including jvm in a Java project.\n\n2. Install for your user\n Install jvm in HOME/.local/bin, with optional PATH and JAVA_HOME setup.\n Useful for keeping jvm available across projects.\n\nChoose [1/2] (default: 2): ' > /dev/tty + IFS= read -r answer < /dev/tty; } 2>/dev/null || return 0 + case $answer in 1) MODE=local; return 0 ;; ''|2) return 0 ;; esac + say 'Please enter 1 or 2.' + done +} consent() { [ "$1" != no ] || return 1 [ "$1" != yes ] || return 0 @@ -292,6 +308,7 @@ installed_jdk() { main() { local host name windows=0 parse "$@" + choose_installation host=$(uname -s) case $host in CYGWIN*|MSYS*|MINGW*) @@ -312,6 +329,10 @@ main() { case $DATA in /*) ;; *) DATA=$HOME/.local/share ;; esac safe_path "$DATA" DEST=$HOME/.local/bin + if [ "$MODE" = local ]; then + DEST=$(pwd -P) || die 'could not resolve current directory' + FILES=(jvm jvm.ps1 jvm.cmd) + fi JAVA_HOME_TARGET=$DATA/jvm/jdks/default JAVA_BIN_TARGET=$JAVA_HOME_TARGET/bin if [ "$windows" = 1 ]; then @@ -344,9 +365,13 @@ main() { chmod 755 "$DEST/jvm" INSTALLED=1 say "jvm $VERSION installed successfully in $DEST." - setup - if ! installed_jdk; then - printf '\nNo JDK is installed yet. Install one with:\n jvm install\n' >&2 + if [ "$MODE" = user ]; then setup; fi + local invocation=jvm + [ "$MODE" != local ] || invocation=./jvm + if installed_jdk; then + printf '\nSee your currently installed JDKs with:\n %s list\n' "$invocation" >&2 + else + printf '\nNo JDK is installed yet. Install one with:\n %s install\n' "$invocation" >&2 fi say 'Setup complete.' if [ "$ENV_CHANGED" = 1 ]; then say 'Open a new terminal application to use your changes.'; fi From b2f44ef0ba42ec0c6830b198e34f62171978547c Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Thu, 5 Nov 2026 20:28:40 +0100 Subject: [PATCH 2/3] fix: fix `env` output in Windows CMD --- README.md | 6 ++++ SPEC.md | 26 +++++++++++++++-- jvm | 20 ++++++------- jvm.cmd | 2 +- jvm.ps1 | 59 ++++++++++++++++++++++++-------------- tests/README.md | 21 ++++++++++++++ tests/powershell.ps1 | 68 +++++++++++++++++++++++++++++++++++++++++++- 7 files changed, 166 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index f85396f..968c521 100644 --- a/README.md +++ b/README.md @@ -208,6 +208,12 @@ jvm [options] [selector] configured selections and unusable defaults are errors. The `_` placeholder is exclusive to exec; it is not a configuration value. Other commands give it no placeholder meaning and retain their existing exact-directory ID rules. + +On Windows, `jvm.ps1 env` prints PowerShell assignments for `iex`. The +`jvm.cmd` launcher requests CMD assignments using a leading `--cmd` signal. +Run `jvm.cmd env temurin-21`, then copy and paste the printed lines into your +CMD window. The PATH statement uses `%PATH%` to retain your current PATH. + `home` and `env` use the same precedence when their selector is omitted. `jvm default` queries the global default; `jvm default 21` queries Java 21. After `set`, one argument is a selector (`jvm default set 21` sets the global diff --git a/SPEC.md b/SPEC.md index 1b98bea..5dfa8b6 100644 --- a/SPEC.md +++ b/SPEC.md @@ -297,6 +297,28 @@ is prepared fully before printing and never evaluated by `jvm`. `jvm.ps1 env` emits equivalent PowerShell `$env:` assignments, for example `iex (& '.\jvm.ps1' env 'temurin-21')`. +The Windows launcher passes a leading `--cmd` signal to `jvm.ps1`. Only that +position consumes the signal; it is removed before command parsing, usage-comment +generation and `exec` argument forwarding. Without it, PowerShell output remains +the default. With it, `env`/`e` prints commands for copying and pasting into a +CMD window: + +```bat +rem Copy ^& paste these lines into your CMD window +set "JAVA_HOME=C:\selected\home" +set "GRAALVM_HOME=C:\selected\home" +set "PATH=C:\selected\home\bin;%PATH%" +``` + +The ampersand in the comment is escaped so the comment itself can also be pasted. +`GRAALVM_HOME` appears only for GraalVM. PATH deduplication and `-f` work +identically to PowerShell output. The PATH statement uses `%PATH%`, expanded by +CMD when pasted, rather than embedding the inherited PATH. No invocation path, +alias or arguments are included in the CMD comment. This mode selects syntax +explicitly, without parent-process shell detection. Running `jvm.cmd` from +PowerShell also requests CMD output; use `jvm.ps1` for PowerShell assignments. +Pasted statements follow normal CMD percent-variable and delayed-expansion rules. + `exec` runs `` with `JAVA_HOME` set and the selected JDK's `bin` first on `PATH` (always prepended). It uses the physical home, never a mutable link, and does not change the parent shell. Automatic installation initializes missing @@ -610,8 +632,8 @@ Last-Modified and last-check metadata. both in one change. - **`jvm.cmd`**: a thin launcher for `cmd.exe` and double-click use. It runs the `jvm.ps1` beside it with `powershell.exe -NoProfile -ExecutionPolicy - Bypass -File`, forwards all arguments and returns its exit code. It contains - no parsing logic. + Bypass -File`, adds the leading `--cmd` output signal, forwards all user + arguments and returns its exit code. It contains no parsing logic. - Fixtures, tests and contributor tooling stay outside the shipped scripts and are never runtime dependencies. No runtime dependency on Java. diff --git a/jvm b/jvm index 11d1ba2..b5c4716 100755 --- a/jvm +++ b/jvm @@ -83,17 +83,17 @@ jvm finds and installs Java Development Kits for this computer. Usage: jvm [options] [selector] Commands: - available, a [-t] [selector] Show compatible JDKs available to download. - install, i [-d] [-v] [selector] Download, verify and install a JDK in user storage. - uninstall, u Remove an installed JDK from user storage. - list, l [selector] Show JDK folders and manifest status, even offline. - default, d [] Query the global or Java-major default. - default, d set [] Set a persistent JDK default. - home, h [selector] Print an installed JDK home or the stable default path. - env, e [-f] [selector] Print Bash exports, including GRAALVM_HOME for GraalVM. + available, a [-t] [selector] Show compatible JDKs available to download. + install, i [-d] [-v] [selector] Download, verify and install a JDK in user storage. + uninstall, u Remove an installed JDK from user storage. + list, l [selector] Show JDK folders and manifest status, even offline. + default, d [] Query the global or Java-major default. + default, d set [] Set a persistent JDK default. + home, h [selector] Print an installed JDK home or the stable default path. + env, e [-f] [selector] Print Bash exports, including GRAALVM_HOME for GraalVM. exec, x [--] [args...] Run with a selected JDK or default. - --help Show this help. - --version Show the jvm version. + --help Show this help. + --version Show the jvm version. default set/home/env/exec install automatically when no installed JDK matches. default queries are offline. After set: one argument is a selector; two are scope and selector. diff --git a/jvm.cmd b/jvm.cmd index 4c85052..0a2fec4 100644 --- a/jvm.cmd +++ b/jvm.cmd @@ -21,5 +21,5 @@ rem LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM rem OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE rem SOFTWARE. -powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0jvm.ps1" %* +powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0jvm.ps1" --cmd %* exit /b %ERRORLEVEL% diff --git a/jvm.ps1 b/jvm.ps1 index 884a6d1..ae5abb6 100644 --- a/jvm.ps1 +++ b/jvm.ps1 @@ -26,6 +26,7 @@ $script:DefaultDistro = 'temurin' $script:DefaultVersion = '25' $script:ScriptInvocation = $MyInvocation +$script:CmdOutput = $false $script:Version = '0.2.0' $script:BaseUrl = 'https://getjvm.dev/index' $script:ExitCode = 1 @@ -1423,17 +1424,23 @@ function Invoke-HomeOrEnvironment { } $javaHome = $resolved.Home $bin = Join-Path $javaHome 'bin' - $invocationPath = Get-EnvironmentInvocationPath - if ($invocationPath -match "[`r`n]") { Stop-Jvm 'script path must be a single-line path' } - $usage = '& ' + (ConvertTo-PowerShellLiteral $invocationPath) - foreach ($argument in $OriginalArguments) { - if ($argument -match '^[a-zA-Z0-9_-]+$') { $usage += ' ' + $argument } - else { $usage += ' ' + (ConvertTo-PowerShellLiteral $argument) } - } - $lines = @("# Use: iex ($usage)", - ('$env:JAVA_HOME = ' + (ConvertTo-PowerShellLiteral $javaHome))) - if ($resolved.Row.JVM -eq 'graalvm') { - $lines += '$env:GRAALVM_HOME = ' + (ConvertTo-PowerShellLiteral $javaHome) + if ($script:CmdOutput) { + $lines = @('rem Copy ^& paste these lines into your CMD window', + ('set "JAVA_HOME=' + $javaHome + '"')) + if ($resolved.Row.JVM -eq 'graalvm') { $lines += 'set "GRAALVM_HOME=' + $javaHome + '"' } + } else { + $invocationPath = Get-EnvironmentInvocationPath + if ($invocationPath -match "[`r`n]") { Stop-Jvm 'script path must be a single-line path' } + $usage = '& ' + (ConvertTo-PowerShellLiteral $invocationPath) + foreach ($argument in $OriginalArguments) { + if ($argument -match '^[a-zA-Z0-9_-]+$') { $usage += ' ' + $argument } + else { $usage += ' ' + (ConvertTo-PowerShellLiteral $argument) } + } + $lines = @("# Use: iex ($usage)", + ('$env:JAVA_HOME = ' + (ConvertTo-PowerShellLiteral $javaHome))) + if ($resolved.Row.JVM -eq 'graalvm') { + $lines += '$env:GRAALVM_HOME = ' + (ConvertTo-PowerShellLiteral $javaHome) + } } $alreadyPresent = $false if (-not $ForcePath) { @@ -1442,7 +1449,11 @@ function Invoke-HomeOrEnvironment { } } if (-not $alreadyPresent) { - $lines += '$env:PATH = ' + (ConvertTo-PowerShellLiteral ($bin + ';')) + ' + $env:PATH' + if ($script:CmdOutput) { + $lines += 'set "PATH=' + $bin + ';%PATH%"' + } else { + $lines += '$env:PATH = ' + (ConvertTo-PowerShellLiteral ($bin + ';')) + ' + $env:PATH' + } } # One complete string also supports Invoke-Expression (& .\jvm env ...). Write-Output ($lines -join "`n") @@ -1659,17 +1670,17 @@ jvm finds and installs Java Development Kits for this computer. Usage: .\jvm [options] [selector] Commands: - available, a [-t] [selector] Show compatible JDKs available to download. - install, i [-d] [-v] [selector] Download, verify and install a JDK. - uninstall, u Remove an installed JDK. - list, l [selector] Show JDK folders and manifest status, offline. - default, d [] Query the global or Java-major default. - default, d set [] Set a persistent default. - home, h [selector] Print an installed JDK home or default path. - env, e [-f] [selector] Print PowerShell environment assignments. + available, a [-t] [selector] Show compatible JDKs available to download. + install, i [-d] [-v] [selector] Download, verify and install a JDK. + uninstall, u Remove an installed JDK. + list, l [selector] Show JDK folders and manifest status, offline. + default, d [] Query the global or Java-major default. + default, d set [] Set a persistent default. + home, h [selector] Print an installed JDK home or default path. + env, e [-f] [selector] Print environment assignments. exec, x [--] [args...] Run with a selected JDK. - --help Show this help. - --version Show the jvm version. + --help Show this help. + --version Show the jvm version. default set/home/env/exec install automatically when no installed JDK matches. default queries are offline. After set: one argument is a selector; two are scope and selector. @@ -1736,6 +1747,10 @@ function Get-DefaultArguments { function Invoke-Jvm { param([string[]]$Arguments) + if ($Arguments.Count -and $Arguments[0] -ceq '--cmd') { + $script:CmdOutput = $true + $Arguments = @($Arguments | Select-Object -Skip 1) + } $command = if ($Arguments.Count) { $Arguments[0] } else { '' } $aliases = @{ a = 'available'; i = 'install'; u = 'uninstall'; l = 'list' d = 'default'; h = 'home'; e = 'env'; x = 'exec' } diff --git a/tests/README.md b/tests/README.md index c1302a8..2d6f5b9 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,5 +1,26 @@ # Discovery, installation and uninstallation verification +## CMD environment output (2026-11-05) + +`jvm.cmd` supplies the leading `--cmd` signal. CMD `env`/`e` now prints a +copy-and-paste comment and `set "PATH=...;%PATH%"` rather than embedding the +inherited PATH or suggesting FOR /F evaluation. The comment escapes its +ampersand so it can also be pasted safely. + +Focused offline regressions passed under Windows PowerShell 5.1. They feed +separate output lines, including the comment, into native CMD and verify PATH +expansion, JAVA_HOME, GraalVM, PATH deduplication and `-f`. They also check the +signal's leading-only position and unchanged PowerShell usage comments from +spaced and apostrophe-containing invocation paths. All fixtures use temporary +HOME/XDG roots and child processes; no real JDK is executed. + +Bash syntax, PowerShell parsing and `git diff --check` passed. The full 30-group +PowerShell suite passed for the preceding CMD implementation; this output-only +revision reran the focused environment cases. ShellCheck is unavailable. No +additional Unix/Cygwin/MSYS2 or Bash 3.2 validation was performed. Distributed +sizes: `jvm` 70,088 bytes, `jvm.ps1` 80,521 bytes and `jvm.cmd` 1,271 bytes, +all retaining LF endings. + ## Installed-JDK reminder (2026-11-05) Both installers now suggest listing the installed JDKs when the managed diff --git a/tests/powershell.ps1 b/tests/powershell.ps1 index 97b0734..801eb11 100644 --- a/tests/powershell.ps1 +++ b/tests/powershell.ps1 @@ -120,6 +120,32 @@ function Invoke-JvmLauncherTest { return [pscustomobject]@{ Out = $outText; Err = $errText } } +function Assert-CmdEnvironmentPaste { + param([string]$Output, [string]$JavaHome, [switch]$GraalVM) + $stdout = Join-Path $testRoot 'cmd-env.out' + $stderr = Join-Path $testRoot 'cmd-env.err' + $stdin = Join-Path $testRoot 'cmd-env.in' + $result = Join-Path $testRoot 'cmd-env.result' + $probePath = Join-Path $testRoot 'cmd environment probe.ps1' + [IO.File]::WriteAllText($probePath, + '$values = @($env:JAVA_HOME, $env:PATH); ' + + 'if ($args.Count -gt 1) { $values += $env:GRAALVM_HOME }; ' + + '[IO.File]::WriteAllLines($args[0], $values)') + $probe = '"' + $PowerShellExecutable + '" -NoProfile -NonInteractive -File "' + + $probePath + '" "' + $result + '"' + $expected = "$JavaHome`r`n" + (Join-Path $JavaHome 'bin') + ';' + $env:PATH + if ($GraalVM) { $probe += ' graalvm'; $expected += "`r`n$JavaHome" } + # Feed separate lines to CMD as if pasted at its prompt, including the comment. + [IO.File]::WriteAllText($stdin, $Output + "`r`n" + $probe + "`r`nexit`r`n") + $process = Start-Process -FilePath $env:ComSpec -NoNewWindow -Wait -PassThru ` + -ArgumentList '/d /q /v:off' -RedirectStandardInput $stdin ` + -RedirectStandardOutput $stdout -RedirectStandardError $stderr + Assert-Equal ([string]$process.ExitCode) '0' 'Pasted CMD statements failed.' + Assert-Equal ([IO.File]::ReadAllText($stderr)) '' 'Pasted CMD statements emitted stderr.' + Assert-Equal ([IO.File]::ReadAllText($result).TrimEnd()) $expected ` + 'Pasted CMD statements did not apply JAVA_HOME and expand PATH.' +} + try { [void][IO.Directory]::CreateDirectory($testRoot) $homeDir = Join-Path $testRoot 'home' @@ -353,11 +379,48 @@ try { } Invoke-JvmTest @('exec', $id, '--', '--', 'probe.exe') -ExpectedExitCode 127 | Out-Null Write-TestPass 'exec and x accept selectors and the placeholder, with one optional separator' - $launcherDirectory = Join-Path $testRoot 'launcher folder' + $launcherDirectory = Join-Path $testRoot "launcher's folder & !test! (x)^" [void][IO.Directory]::CreateDirectory($launcherDirectory) $launcherPath = Join-Path $launcherDirectory 'jvm.cmd' Copy-Item -LiteralPath (Join-Path $repository 'jvm.cmd') -Destination $launcherPath Copy-Item -LiteralPath $scriptPath -Destination (Join-Path $launcherDirectory 'jvm.ps1') + foreach ($alias in @('env', 'e')) { + $streams = Invoke-JvmLauncherTest ($alias + ' -f ' + $id) + Assert-Equal $streams.Err '' 'CMD env emitted stderr.' + $lines = $streams.Out -split "`r?`n" + Assert-Equal $lines[1] ('set "JAVA_HOME=' + $install + '"') 'CMD env omitted JAVA_HOME.' + Assert-Equal $lines[2] ('set "PATH=' + (Join-Path $install 'bin') + ';%PATH%"') ` + 'CMD env did not retain the PATH reference.' + Assert-Equal $lines[0] 'rem Copy ^& paste these lines into your CMD window' 'CMD copy-and-paste comment mismatch.' + Assert-CmdEnvironmentPaste $streams.Out $install + } + $savedPath = $env:PATH + try { + $env:PATH = (Join-Path $install 'bin') + ';' + $savedPath + $streams = Invoke-JvmLauncherTest ('env ' + $id) + if ($streams.Out -match '(?m)^set "PATH=') { throw 'CMD env repeated an existing bin entry.' } + $streams = Invoke-JvmLauncherTest ('env -f ' + $id) + if ($streams.Out -notmatch '(?m)^set "PATH=') { throw 'CMD env -f omitted PATH.' } + } finally { $env:PATH = $savedPath } + $savedData = $env:XDG_DATA_HOME + try { + $env:PATH = $savedPath + ';C:\literal %PATH% !bang! (x)^' + $env:XDG_DATA_HOME = Join-Path $testRoot 'data !bang! & (jdk)^' + $graalId = '21.0.1-graalvm' + $graalHome = Join-Path $env:XDG_DATA_HOME ('jvm\jdks\' + $graalId) + [void][IO.Directory]::CreateDirectory((Split-Path -Parent $graalHome)) + Copy-Item -LiteralPath $install -Destination $graalHome -Recurse + $graalFields = @($fields) + $graalFields[4] = 'graalvm'; $graalFields[6] = 'graalvm' + [IO.File]::WriteAllText((Join-Path $graalHome '.jvm-manifest'), + "jvm-manifest-v1`n$($graalFields -join "`t")`n`n", [Text.UTF8Encoding]::new($false)) + $streams = Invoke-JvmLauncherTest ('env -f ' + $graalId) + Assert-CmdEnvironmentPaste $streams.Out $graalHome -GraalVM + } finally { $env:XDG_DATA_HOME = $savedData; $env:PATH = $savedPath } + [void](Invoke-JvmTest @('env', '--cmd', $id) -ExpectedExitCode 2) + [void](Invoke-JvmTest @('--cmd', '--cmd', 'env', $id) -ExpectedExitCode 2) + [void](Invoke-JvmLauncherTest '--version') + Write-TestPass 'CMD env supports copy and paste, PATH expansion, GraalVM and PATH deduplication' foreach ($alias in @('exec', 'x')) { foreach ($separator in @('', '-- ')) { $prefix = $alias + ' _ ' + $separator + 'probe.exe -NoProfile -NonInteractive -File "' + $argumentProbe + '" ' @@ -369,6 +432,9 @@ try { [void](Invoke-JvmLauncherTest ($prefix + '"--" "$(1+2) & * --help"')) Assert-Equal ([IO.File]::ReadAllText($argsFile)) '<-->|<$(1+2) & * --help>' ` 'jvm.cmd interpreted literal child arguments.' + [void](Invoke-JvmLauncherTest ($prefix + '"--cmd" "two words"')) + Assert-Equal ([IO.File]::ReadAllText($argsFile)) '<--cmd>|' ` + 'jvm.cmd consumed the output signal from child arguments.' } } [void](Invoke-JvmLauncherTest 'x _ probe.exe -NoProfile -NonInteractive -Command "exit 37"' 37) From d2a8e551edb11d029194021f692f7d92cf7e612a Mon Sep 17 00:00:00 2001 From: Tako Schotanus Date: Thu, 5 Nov 2026 20:30:17 +0100 Subject: [PATCH 3/3] chore: minor title change in web/index.html --- web/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/index.html b/web/index.html index f9b81e4..2786e91 100644 --- a/web/index.html +++ b/web/index.html @@ -5,7 +5,7 @@ - jvm — Java for your projects + jvm — Java Version Manager