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
4 changes: 4 additions & 0 deletions INSTALLER_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
25 changes: 19 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.

Expand Down Expand Up @@ -201,6 +208,12 @@ jvm <command> [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
Expand Down
69 changes: 57 additions & 12 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<command>` 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
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -650,27 +672,41 @@ 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
```

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
Expand Down Expand Up @@ -711,14 +747,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
Expand All @@ -731,7 +771,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.
Expand All @@ -751,8 +791,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
Expand All @@ -768,7 +808,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
Expand Down
20 changes: 10 additions & 10 deletions jvm
Original file line number Diff line number Diff line change
Expand Up @@ -83,17 +83,17 @@ jvm finds and installs Java Development Kits for this computer.
Usage: jvm <command> [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 <selector> Remove an installed JDK from user storage.
list, l [selector] Show JDK folders and manifest status, even offline.
default, d [<major>] Query the global or Java-major default.
default, d set [<major>] <selector> 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 <selector> Remove an installed JDK from user storage.
list, l [selector] Show JDK folders and manifest status, even offline.
default, d [<major>] Query the global or Java-major default.
default, d set [<major>] <selector> 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 <selector> [--] <command> [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.
Expand Down
2 changes: 1 addition & 1 deletion jvm.cmd
Original file line number Diff line number Diff line change
Expand Up @@ -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%
59 changes: 37 additions & 22 deletions jvm.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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")
Expand Down Expand Up @@ -1659,17 +1670,17 @@ jvm finds and installs Java Development Kits for this computer.
Usage: .\jvm <command> [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 <selector> Remove an installed JDK.
list, l [selector] Show JDK folders and manifest status, offline.
default, d [<major>] Query the global or Java-major default.
default, d set [<major>] <selector> 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 <selector> Remove an installed JDK.
list, l [selector] Show JDK folders and manifest status, offline.
default, d [<major>] Query the global or Java-major default.
default, d set [<major>] <selector> Set a persistent default.
home, h [selector] Print an installed JDK home or default path.
env, e [-f] [selector] Print environment assignments.
exec, x <selector> [--] <command> [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.
Expand Down Expand Up @@ -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' }
Expand Down
Loading
Loading