Documentation
¶
Overview ¶
Package deps makes avar's backend dependency invisible.
avar drives its backend as a subprocess, so the backend's command-line tool is a hard requirement: `limactl` on macOS, `wsl.exe` on Windows. This package locates the one this host needs, gates it on a pinned minimum version, and — only with the user's explicit consent — installs or updates it. A user who has never heard of Lima or of WSL should never have to read a README to get past a missing dependency (REQ-8, REQ-18.2, REQ-18.3).
One host, one dependency. A Windows invocation checks WSL and never mentions Lima, Docker Desktop, or any other virtualization runtime, and a macOS invocation is the mirror image (PROP-13). The two managers therefore share no state and no decisions: they share only the vocabulary below — Runner, Version, and the consent prompt — because "ask before changing the user's machine" is the one rule both obey.
Nothing here builds a command line for a shell to interpret. Every subprocess is executed as an argv, so no path, version string, or tool output can turn into shell syntax.
Nothing here creates an environment, either. A dependency manager's job ends when it can say "the backend is usable" or "here is what to do about it"; registering a machine or a distribution is the provider's, which is what makes an interrupted or restart-blocked setup leave nothing half-made behind (REQ-18.3, PROP-13).
Index ¶
- Constants
- Variables
- func DecodeWSLOutput(b []byte) string
- type Lima
- type Manager
- type NotInstalledError
- type NotInstalledReason
- type Runner
- type Version
- type VersionTooOldError
- type WSL
- type WSL1Error
- type WSLManager
- type WSLNotInstalledError
- type WSLNotInstalledReason
- type WSLRestartRequiredError
- type WSLVersionTooOldError
Constants ¶
const MinLimaVersion = "2.0.0"
MinLimaVersion is the oldest Lima release avar supports. It is the single point of change for the version gate: nothing else in avar hard-codes a Lima version.
The floor is 2.0.0 because 2.x is the only surface avar has evidence for. avar's generated configurations are checked with `limactl validate` inside the test suite, and that evidence comes from Lima 2.x; the guest account behaviour REQ-1.4 depends on is asserted the same way. Lima 1.x appears to carry every field and flag avar uses, but "appears to, from reading its source" is not the same standard, and a floor that admits a version nothing is ever tested against is not a gate at all — it only defers the failure to a user's machine.
Templates emit this value as `minimumLimaVersion`, so raising the constant also stops an older Lima from accepting a configuration written for a newer one. Lowering it again is fine once 1.x is genuinely exercised.
const MinWSLVersion = "2.0.0"
MinWSLVersion is the oldest WSL release avar supports. It is the single point of change for the version gate: nothing else in avar hard-codes a WSL version.
The floor is 2.0.0 for the same reason Lima's is 2.0.0 — it is the oldest release avar has evidence for — but here the number also draws a real line. WSL is delivered two ways: as an optional Windows component, which is what a machine that has never run `wsl --update` has, and as a Store package, which Microsoft has shipped since late 2023 and which is the only channel carrying a `wsl --version` at all. Every flag the WSL2Provider is built on belongs to the Store releases: --cd for the guest working directory, --import --version 2 for creating an avar-owned distribution, and --export --vhd for a snapshot. A build that cannot report its own version cannot have them, and guessing would move the failure from a sentence avar can explain to a subprocess error the user has to decode.
Variables ¶
var ErrLimaNotFound = errors.New("limactl not found on PATH or in the Homebrew binary directories")
ErrLimaNotFound reports that no limactl executable could be located.
var ErrRestartRequired = errors.New("WSL needs Windows to restart before it can be used")
ErrRestartRequired reports that WSL setup got as far as it can without the machine being restarted.
It is deliberately not an installation failure. The install succeeded; the platform is not usable until Windows has restarted, and the user's next step is to reboot and run the same avr command again. Callers match on this to say exactly that, rather than reporting that something went wrong (REQ-18.3).
var ErrWSL1 = errors.New("distribution is registered as WSL 1")
ErrWSL1 reports a distribution registered as WSL 1.
avar never converts one: `wsl --set-version` rewrites the distribution's entire filesystem in place and takes minutes. Doing that on the user's behalf, as a side effect of them asking for a shell, is not a decision avar gets to make (REQ-18.4, PROP-15).
var ErrWSLNotFound = errors.New("wsl.exe not found on PATH or in the Windows system directory")
ErrWSLNotFound reports that no wsl.exe could be located at all.
Functions ¶
func DecodeWSLOutput ¶ added in v0.3.0
DecodeWSLOutput turns the bytes wsl.exe wrote into text, decoding UTF-16 when that is what it produced.
It is exported because the WSL2Provider parses the same tool's output, and a second copy of this is how one of the two ends up reading distribution names as runs of NUL-separated characters.
wsl.exe writes UTF-16LE when its output is redirected — which it always is, for avar — and it writes it with no byte-order mark, so nothing declares the encoding and the bytes have to be recognised. Verified against WSL 2.7.12, where `wsl --version` begins 57 00 53 00 4C 00: "WSL" in UTF-16LE. Some newer output is UTF-8, and is handled by the same function taking the other branch, which is why the encoding is detected rather than assumed.
Detection is by NUL bytes rather than by any heuristic on the text, because that distinction is unambiguous: no output avar reads contains a NUL byte in UTF-8, and UTF-16LE text made of ASCII is half NUL bytes, all at odd offsets. A byte-order mark, where one is present, is authoritative and is consumed.
Types ¶
type Lima ¶
type Lima struct {
// Path is the absolute path to the limactl executable that was verified.
// Callers execute this path rather than resolving "limactl" again, so the
// binary avar checked is the binary avar runs.
Path string
// Version is what `limactl --version` reported.
Version Version
}
Lima describes a usable Lima installation.
type Manager ¶
type Manager struct {
// Runner executes limactl and brew. Defaults to a real exec runner.
Runner Runner
// Confirm asks the user a yes/no question. Defaults to a prompt on
// stdin/Out. It must return false when the answer is no.
Confirm func(ctx context.Context, question string) (bool, error)
// Interactive reports whether there is a user to ask. Defaults to
// "stdin is a terminal".
Interactive func() bool
// LookPath resolves an executable name. Defaults to exec.LookPath.
LookPath func(file string) (string, error)
// FallbackDirs are searched for limactl when LookPath fails. Defaults to
// the Homebrew binary directories.
FallbackDirs []string
// Out receives the install prompt's progress and the installer's own
// output. Defaults to io.Discard.
Out io.Writer
}
Manager answers one question: is there a Lima that avar can use, and if not, what happens next.
Every collaborator that touches the outside world is a field, so the whole component is testable without a subprocess, a terminal, or Homebrew. A zero Manager uses the real ones.
func (*Manager) EnsureLima ¶
EnsureLima returns a Lima installation that avar can operate against.
A compatible Lima is used as-is, silently. A missing Lima produces an offer to install it with Homebrew, which proceeds only after the user says yes. An unsupported version is refused outright: avar never auto-upgrades and never operates against a Lima below MinLimaVersion.
type NotInstalledError ¶
type NotInstalledError struct {
Reason NotInstalledReason
// Err is the underlying failure, when the reason is an install that did
// not work out.
Err error
}
NotInstalledError reports that avar cannot proceed because Lima is not installed. Its message carries the manual installation instructions, because this error is the last thing the user sees before avar exits non-zero.
func (*NotInstalledError) Error ¶
func (e *NotInstalledError) Error() string
func (*NotInstalledError) Unwrap ¶
func (e *NotInstalledError) Unwrap() error
type NotInstalledReason ¶
type NotInstalledReason string
NotInstalledReason says why avar has no usable Lima and did not install one.
const ( // ReasonDeclined: the user was asked and said no. ReasonDeclined NotInstalledReason = "declined" // ReasonNoHomebrew: Homebrew is not available to install with. ReasonNoHomebrew NotInstalledReason = "no-homebrew" // ReasonNonInteractive: there was nobody to ask for consent. ReasonNonInteractive NotInstalledReason = "non-interactive" // ReasonInstallFailed: `brew install lima` returned an error. ReasonInstallFailed NotInstalledReason = "install-failed" // ReasonInstallIncomplete: the install reported success but avar still // cannot find or read a Lima installation. ReasonInstallIncomplete NotInstalledReason = "install-incomplete" )
type Runner ¶
type Runner interface {
// Output runs the program and returns its standard output.
Output(ctx context.Context, name string, args ...string) ([]byte, error)
// Stream runs the program, copying its output to w as it arrives so a
// long install shows progress instead of looking like a hang.
Stream(ctx context.Context, w io.Writer, name string, args ...string) error
}
Runner executes an external program.
Implementations receive a program path and an argv — never a command string — which is what keeps a shell out of avar's subprocess handling. Both methods must honour ctx by terminating the child rather than leaking it.
func NewRunner ¶
func NewRunner() Runner
NewRunner returns the production Runner, which executes a program with an argv and never involves a shell.
It exists because every caller that drives a backend needs the same runner this package already uses for limactl, and the alternative — each package writing its own os/exec wrapper — would duplicate the parts that are easy to get wrong: folding stderr into the error, killing the child when the context is cancelled, and bounding how long a finished child's pipes may keep avar waiting.
type Version ¶
type Version struct {
Major int
Minor int
Patch int
// Pre is the pre-release identifier set without its leading '-',
// e.g. "beta.1" from "1.1.0-beta.1".
Pre string
// Build is the build metadata without its leading '+', e.g. "dirty".
Build string
}
Version is a semantic version as reported by a tool's version output. Build metadata is retained so avar can echo back exactly what it found, but it takes no part in precedence.
func MinWSL ¶ added in v0.3.0
func MinWSL() Version
MinWSL returns the minimum supported WSL version.
func ParseVersion ¶
ParseVersion extracts a version from a tool's version output.
It is deliberately tolerant: `limactl --version` prints "limactl version 1.0.4", but the wording, a leading "v", pre-release and build suffixes, and trailing whitespace or punctuation are all things a future release could change without avar caring. The first version-shaped token wins.
type VersionTooOldError ¶
type VersionTooOldError struct {
Found Version
Minimum Version
// Path is the limactl that reported Found.
Path string
}
VersionTooOldError reports a Lima installation below MinLimaVersion. avar refuses to operate against it rather than risk misreading limactl's output.
func (*VersionTooOldError) Error ¶
func (e *VersionTooOldError) Error() string
type WSL ¶ added in v0.3.0
type WSL struct {
// Path is the absolute path to the wsl.exe that was verified. Callers
// execute this path rather than resolving "wsl.exe" again, so the binary
// avar checked is the binary avar drives.
Path string
// Version is what `wsl --version` reported.
Version Version
}
WSL describes a usable WSL installation.
type WSL1Error ¶ added in v0.3.0
type WSL1Error struct {
// Distribution is the registered name of the WSL 1 distribution.
Distribution string
}
WSL1Error reports an avar-owned distribution registered as WSL 1, and carries the exact command that converts it.
avar names the command and stops. The conversion rewrites the distribution's filesystem and takes minutes, so it is the user's decision, not a side effect of asking for a shell (REQ-18.4, PROP-15).
It lives here rather than with the provider because it is a statement about a prerequisite, and because the remedy belongs beside the other remedies avar offers for the same dependency. The provider decides which distribution is in this state; what to say about it is settled once, here.
type WSLManager ¶ added in v0.3.0
type WSLManager struct {
// Runner executes wsl.exe. Defaults to a real exec runner.
Runner Runner
// Confirm asks the user a yes/no question. Defaults to a prompt on
// stdin/Out. It must return false when the answer is no.
Confirm func(ctx context.Context, question string) (bool, error)
// Interactive reports whether there is a user to ask. Defaults to
// "stdin is a terminal".
Interactive func() bool
// LookPath resolves an executable name. Defaults to exec.LookPath.
LookPath func(file string) (string, error)
// FallbackDirs are searched for wsl.exe when LookPath fails. Defaults to
// the Windows system directory.
FallbackDirs []string
// Out receives the setup prompt's progress and the tool's own output.
// Defaults to io.Discard.
Out io.Writer
}
WSLManager answers one question: is there a WSL that avar can use, and if not, what happens next.
Every collaborator that touches the outside world is a field, so the whole component is testable without a subprocess, a terminal, or a working WSL — which matters more here than it does for Lima, since the states worth testing (absent, disabled, too old, restart pending) are states a developer's own machine is in only one of, and cannot be moved between cheaply. A zero WSLManager uses the real ones.
It is a separate type from Manager rather than a mode of it. The two share the rule "ask before changing the user's machine" and nothing else: different tool, different setup commands, different search path, and a restart outcome that has no counterpart in `brew install`. Folding them together would let a Lima test reach a WSL field, and would put a host branch inside a component whose whole purpose is that a Windows invocation never mentions Lima (PROP-13).
func (*WSLManager) EnsureWSL ¶ added in v0.3.0
func (m *WSLManager) EnsureWSL(ctx context.Context) (WSL, error)
EnsureWSL returns a WSL installation that avar can operate against.
A compatible WSL is used as-is, silently — that is the warm path of every Windows invocation and it costs one subprocess. Anything else is a question put to the user before avar changes their machine: a WSL that cannot report a version offers an install, one below the minimum offers an update, and neither proceeds without an explicit yes.
Nothing is registered here whatever the outcome. A refusal, a declined prompt, and a restart-pending install all leave the machine as they found it apart from the platform setup the user agreed to (REQ-18.3, PROP-13).
type WSLNotInstalledError ¶ added in v0.3.0
type WSLNotInstalledError struct {
Reason WSLNotInstalledReason
// Action is "install" or "update", when the reason is a setup that did not
// work out.
Action string
// Err is the underlying failure, where there was one.
Err error
}
WSLNotInstalledError reports that avar cannot proceed because WSL is not usable. Its message carries the manual instructions, because this error is the last thing the user sees before avar exits non-zero.
func (*WSLNotInstalledError) Error ¶ added in v0.3.0
func (e *WSLNotInstalledError) Error() string
func (*WSLNotInstalledError) Unwrap ¶ added in v0.3.0
func (e *WSLNotInstalledError) Unwrap() error
type WSLNotInstalledReason ¶ added in v0.3.0
type WSLNotInstalledReason string
WSLNotInstalledReason says why avar has no usable WSL and did not set one up.
const ( // WSLReasonNotFound: wsl.exe could not be found at all, which on a supported // Windows host means something is wrong with the installation itself. WSLReasonNotFound WSLNotInstalledReason = "no-wsl" // WSLReasonDeclined: the user was asked and said no. WSLReasonDeclined WSLNotInstalledReason = "declined" // WSLReasonNonInteractive: there was nobody to ask for consent. WSLReasonNonInteractive WSLNotInstalledReason = "non-interactive" // WSLReasonSetupFailed: the install or update returned an error. WSLReasonSetupFailed WSLNotInstalledReason = "setup-failed" )
type WSLRestartRequiredError ¶ added in v0.3.0
type WSLRestartRequiredError struct {
// Err is the re-probe that still could not read a version, kept so the
// underlying condition stays inspectable.
Err error
}
WSLRestartRequiredError reports that setup succeeded but the platform is not usable until Windows restarts.
The instruction it carries is idempotent on purpose: running the same avr command after the restart is the whole of what the user has to do, because nothing was registered and nothing is half-finished (REQ-18.3, PROP-13).
func (*WSLRestartRequiredError) Error ¶ added in v0.3.0
func (e *WSLRestartRequiredError) Error() string
func (*WSLRestartRequiredError) Unwrap ¶ added in v0.3.0
func (e *WSLRestartRequiredError) Unwrap() error
type WSLVersionTooOldError ¶ added in v0.3.0
type WSLVersionTooOldError struct {
Found Version
Minimum Version
// Path is the wsl.exe that reported Found.
Path string
}
WSLVersionTooOldError reports a WSL installation below MinWSLVersion that updating did not fix. avar refuses to operate against it rather than drive flags it may not have.
func (*WSLVersionTooOldError) Error ¶ added in v0.3.0
func (e *WSLVersionTooOldError) Error() string