# Copyright Kevin Deldycke <[email protected]> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
from __future__ import annotations
from functools import cached_property
from pathlib import Path
from extra_platforms import ALL_PLATFORMS
from ..manager import PackageManager
TYPE_CHECKING = False
if TYPE_CHECKING:
from collections.abc import Iterator
from ..package import Package
[docs]
class Go(PackageManager):
"""The Go toolchain, wrapped for the binaries `go install` puts on the machine.
A package here is a *command*, not a library: `go install <path>@<version>`
compiles one and writes it into the toolchain's binary directory, where it
stays until the file is deleted. Libraries are a module's own business and
are scoped to the module that declares them, so they are outside what a
system package manager can address.
```{note}
The inventory is `go version -m`, not `go list`. Pointed at a directory,
`go version` walks it recursively and reports the build information Go
embeds in every binary it produced, which over the binary directory is
exactly the set `go install` created. `go list` answers for the module in
the working directory instead, so it reports nothing about the host.
```
```{caution}
Go has no uninstall verb, in any scope: removing a command means deleting
its file, which is not something a wrapper should do behind the tool's
back. Nothing reports staleness either. Both absences are why
[nao1215/gup](https://github.com/nao1215/gup) exists as a separate tool.
```
Documentation: [go install](https://go.dev/ref/mod#go-install).
"""
name = "Go"
homepage_url = "https://go.dev"
platforms = ALL_PLATFORMS
requirement = ">=1.16.0"
"""`go install` only accepts a version suffix from Go `1.16`.
Before it, the command built whatever the current directory's module
resolved to, so a machine-wide install was not expressible. The release
notes introduce the change as: "`go install` now accepts arguments with
version suffixes (for example, `go install example.com/[email protected]`). This
causes `go install` to build and install packages in module-aware mode,
ignoring the `go.mod` file in the current directory or any parent
directory, if there is one."
"""
cli_names = ("go",)
version_regexes = (r"go\s+version\s+go(?P<version>\S+)\s",)
"""
```{code-block} shell-session
$ go version
go version go1.27.0 darwin/arm64
```
"""
@property
def installed(self) -> Iterator[Package]:
"""Fetch installed packages.
Each binary produces a multi-line record: a header naming the file, a
`path` line carrying the import path `install` accepts, a `mod` line
carrying the module that provided it with its version, then `build`
settings and one `dep` line per transitive dependency.
The id comes from `path` and the version from `mod`, which is why this
cannot be one regex per line. The two agree whenever a module's main
package sits at its root, as both samples below do, but a command
installed from a subdirectory has a `path` ending in that subdirectory
while `mod` stops at the module. `dep` lines are the dependency tree of
the command rather than commands themselves, and are skipped.
```{code-block} shell-session
$ go version -m /tmp/mpm-drive/go/bin
/tmp/mpm-drive/go/bin/gup: go1.27.0
path github.com/nao1215/gup
mod github.com/nao1215/gup v1.8.1 h1:An1c11EdZ36h40GDhtbx5NSRwbKvaEbjwOODwkBDjw4=
dep github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
dep github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
dep github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
dep github.com/gen2brain/beeep v0.11.2 h1:+KfiKQBbQCuhfJFPANZuJ+oxsSKAYNe88hIpJuyKWDA=
dep github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA=
dep github.com/jackmordaunt/icns/v3 v3.0.1 h1:xxot6aNuGrU+lNgxz5I5H0qSeCjNKp8uTXB1j8D4S3o=
dep github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
dep github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
dep github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
dep github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
dep github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
dep github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
dep go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
dep golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
build -buildmode=exe
build -compiler=gc
build DefaultGODEBUG=cryptocustomrand=1,tlssecpmlkem=0,tracebacklabels=0,urlstrictcolons=0,x509sslcertoverrideplatform=0
build CGO_ENABLED=1
build CGO_CFLAGS=
build CGO_CPPFLAGS=
build CGO_CXXFLAGS=
build CGO_LDFLAGS=
build GOARCH=arm64
build GOOS=darwin
build GOARM64=v8.0
/tmp/mpm-drive/go/bin/hello: go1.27.0
path golang.org/x/example/hello
mod golang.org/x/example/hello v0.0.0-20250915201037-7f05d217867b h1:+gZE2jOdiscYByu0606Uw8Ldir2Cecd39Vq/3IEasRA=
build -buildmode=exe
build -compiler=gc
build DefaultGODEBUG=containermaxprocs=0,cryptocustomrand=1,decoratemappings=0,gotestjsonbuildtext=1,httpcookiemaxnum=0,httplaxcontentlength=1,httpmuxgo121=1,httpservecontentkeepheaders=1,multipathtcp=0,panicnil=1,randseednop=0,rsa1024min=0,tlsmlkem=0,tlssecpmlkem=0,tlssha1=1,tracebacklabels=0,updatemaxprocs=0,urlmaxqueryparams=0,urlstrictcolons=0,winreadlinkvolume=0,winsymlink=0,x509negativeserial=1,x509rsacrt=0,x509sha256skid=0,x509sslcertoverrideplatform=0,x509usepolicies=0
build CGO_ENABLED=1
build CGO_CFLAGS=
build CGO_CPPFLAGS=
build CGO_CXXFLAGS=
build CGO_LDFLAGS=
build GOARCH=arm64
build GOOS=darwin
build GOARM64=v8.0
```
"""
package_id = None
for line in self.run_cli("version", "-m", self.bin_dir).splitlines():
# A record header names the file and carries no tab, so it both ends
# the previous record and cannot be mistaken for a field line.
if "\t" not in line:
package_id = None
continue
# Leading whitespace is ignored rather than matched: the tool indents
# every field line with one tab, but ruff rewrites a docstring's
# indentation, so the sample above reaches the corpus with spaces.
fields = line.strip().split("\t")
if fields[0] == "path":
package_id = fields[1]
elif fields[0] == "mod" and package_id:
yield self.package(id=package_id, installed_version=fields[2])
package_id = None
[docs]
@cached_property
def bin_dir(self) -> str:
"""Locate the directory `go install` writes commands into.
`GOBIN` wins when it is set, and is empty on a stock installation,
where the destination is the `bin` subdirectory of the first `GOPATH`
entry instead.
```{code-block} shell-session
$ go env GOBIN
```
```{code-block} shell-session
$ go env GOPATH
/tmp/mpm-drive/go
```
"""
gobin = self.run_cli("env", "GOBIN", force_exec=True).strip()
if gobin:
return gobin
gopath = self.run_cli("env", "GOPATH", force_exec=True).strip()
return str(Path(gopath) / "bin")
[docs]
def install(self, package_id: str, version: str | None = None) -> str:
"""Install one package.
The version suffix is mandatory outside a module, so an unpinned
install asks for `@latest` explicitly rather than omitting it.
```{code-block} shell-session
$ go install golang.org/x/example/hello@latest
```
"""
return self.run_cli("install", f"{package_id}@{version or 'latest'}")