diff --git a/README.md b/README.md
index 3eef22b7..64f26224 100644
--- a/README.md
+++ b/README.md
@@ -40,6 +40,7 @@ Terraform, Terragrunt and Atmos.
- [Semver 2.0.0](https://semver.org/) Compatibility: Utilizes [go-version](https://github.com/hashicorp/go-version) for semantic versioning and use the [HCL](https://github.com/hashicorp/hcl) parser to extract required version constraint from OpenTofu/Terraform/Terragrunt files (see [required_version](#required_version) and [Terragrunt hcl](#terragrunt-hcl-file)).
- Signature verification: Supports [cosign](https://github.com/sigstore/cosign) (if present on your machine) and PGP (via [gopenpgp](https://github.com/ProtonMail/gopenpgp)), see [signature support](#signature-support).
- Intuitive installation: Simple installation process with Homebrew and manual options.
+- Callable as [Go](https://go.dev) module, with a [Semver compatibility promise](https://semver.org/#summary) on [tenvlib](https://github.com/tofuutils/tenv/tree/main/versionmanager/tenvlib) wrapper package (get more information in [TENV_AS_LIB.md](https://github.com/tofuutils/tenv/blob/main/TENV_AS_LIB.md)).
### Difference with asdf
diff --git a/TENV_AS_LIB.md b/TENV_AS_LIB.md
new file mode 100644
index 00000000..6f0fe6a1
--- /dev/null
+++ b/TENV_AS_LIB.md
@@ -0,0 +1,71 @@
+# How to use tenv as a library
+
+## Get started
+
+### Prerequisites
+
+**tenv** requires [Go](https://go.dev) version [1.21](https://go.dev/doc/devel/release#go1.21.0) or above.
+
+### Getting tenv module
+
+`tenvlib` package is available since tenv v3.2
+
+```console
+go get -u github.com/tofuutils/tenv/v3@latest
+```
+
+### Basic example
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/tofuutils/tenv/v3/config/cmdconst"
+ "github.com/tofuutils/tenv/v3/versionmanager/tenvlib"
+)
+
+func main() {
+ tenv, err := tenvlib.Make(tenvlib.AutoInstall, tenvlib.IgnoreEnv, tenvlib.DisableDisplay)
+ if err != nil {
+ fmt.Println("init failed :", err)
+ return
+ }
+
+ err = tenv.DetectedCommandProxy(context.Background(), cmdconst.TofuName, "version")
+ if err != nil {
+ fmt.Println("proxy call failed :", err)
+ }
+}
+```
+
+## Documentation
+
+See the [API documentation on go.dev](https://pkg.go.dev/github.com/tofuutils/tenv/v3/versionmanager/tenvlib) and [examples](https://github.com/tofuutils/tenv/tree/main/versionmanager/tenvlib/examples).
+
+### Overview
+
+Available Tenv struct creation options :
+
+- `AddTool(toolName string, builderFunc builder.BuilderFunc)`, extend `tenvlib` to support other tool use cases.
+- `AutoInstall`, shortcut to force auto install feature enabling in `config.Config`.
+- `DisableDisplay`, do not display or log anything.
+- `IgnoreEnv`, ignore **tenv** environment variables (`TENV_AUTO_INSTALL`, `TOFUENV_TOFU_VERSION`, etc.).
+- `WithConfig(conf *config.Config)`, replace default `Config` (one from a `InitConfigFromEnv` or `DefaultConfig` call depending on `IgnoreEnv` usage).
+- `WithDisplayer(displayer loghelper.Displayer)`, replace default `Displayer` with a custom to handle `tenvlib` output (standard and log).
+- `WithHCLParser(hclParser *hclparse.Parser)`, use passed `Parser` instead of creating a new one.
+
+Tenv methods list :
+
+- `[Detected]Command[Proxy]`
+- `Detect`
+- `Evaluate`
+- `Install[Multiple]`
+- `List[Local|Remote]`
+- `LocallyInstalled`
+- `[Res|S]etDefault[Constraint|Version]`, manage `constraint` and `version` files in `//`
+- `Uninstall[Multiple]`
+
+Happy hacking !
diff --git a/cmd/tenv/subcmd.go b/cmd/tenv/subcmd.go
index 899215d8..95ca626d 100644
--- a/cmd/tenv/subcmd.go
+++ b/cmd/tenv/subcmd.go
@@ -20,6 +20,7 @@ package main
import (
"bytes"
+ "context"
"os"
"strconv"
"strings"
@@ -88,7 +89,8 @@ func newDetectCmd(conf *config.Config, versionManager versionmanager.VersionMana
conf.InitDisplayer(false)
conf.InitInstall(forceInstall, forceNoInstall)
- detectedVersion, err := versionManager.Detect(false)
+ ctx := context.Background()
+ detectedVersion, err := versionManager.Detect(ctx, false)
if err != nil {
loghelper.StdDisplay(err.Error())
@@ -138,6 +140,7 @@ If a parameter is passed, available options:
Run: func(_ *cobra.Command, args []string) {
conf.InitDisplayer(false)
+ ctx := context.Background()
if len(args) == 0 {
version, err := versionManager.Resolve(semantic.LatestKey)
if err != nil {
@@ -146,14 +149,14 @@ If a parameter is passed, available options:
return
}
- if err = versionManager.Install(version); err != nil {
+ if err = versionManager.Install(ctx, version); err != nil {
loghelper.StdDisplay(err.Error())
}
return
}
- if err := versionManager.Install(args[0]); err != nil {
+ if err := versionManager.Install(ctx, args[0]); err != nil {
loghelper.StdDisplay(err.Error())
}
},
@@ -244,7 +247,8 @@ func newListRemoteCmd(conf *config.Config, versionManager versionmanager.Version
Run: func(_ *cobra.Command, _ []string) {
conf.InitDisplayer(false)
- versions, err := versionManager.ListRemote(reverseOrder)
+ ctx := context.Background()
+ versions, err := versionManager.ListRemote(ctx, reverseOrder)
if err != nil {
loghelper.StdDisplay(err.Error())
@@ -378,7 +382,8 @@ Available parameter options:
conf.InitDisplayer(false)
conf.InitInstall(forceInstall, forceNoInstall)
- if err := versionManager.Use(args[0], workingDir); err != nil {
+ ctx := context.Background()
+ if err := versionManager.Use(ctx, args[0], workingDir); err != nil {
loghelper.StdDisplay(err.Error())
}
},
diff --git a/cmd/tenv/tenv.go b/cmd/tenv/tenv.go
index fdcf4f30..9a3153d0 100644
--- a/cmd/tenv/tenv.go
+++ b/cmd/tenv/tenv.go
@@ -19,6 +19,7 @@
package main
import (
+ "context"
"fmt"
"os"
"path/filepath"
@@ -157,7 +158,8 @@ func manageNoArgsCmd(conf *config.Config, hclParser *hclparse.Parser) {
return
}
- if err := toolUI(conf, hclParser); err != nil {
+ ctx := context.Background()
+ if err := toolUI(ctx, conf, hclParser); err != nil {
fmt.Println(err.Error())
os.Exit(1)
diff --git a/cmd/tenv/textui.go b/cmd/tenv/textui.go
index 5878490b..066a5296 100644
--- a/cmd/tenv/textui.go
+++ b/cmd/tenv/textui.go
@@ -19,6 +19,7 @@
package main
import (
+ "context"
"fmt"
"io"
"slices"
@@ -172,7 +173,7 @@ func (m itemModel) View() string {
return "\n" + m.list.View()
}
-func toolUI(conf *config.Config, hclParser *hclparse.Parser) error {
+func toolUI(ctx context.Context, conf *config.Config, hclParser *hclparse.Parser) error {
conf.InitDisplayer(false)
// shared object
@@ -212,7 +213,7 @@ func toolUI(conf *config.Config, hclParser *hclparse.Parser) error {
for _, toolItem := range tools {
tool := toolItem.FilterValue()
if _, selected := selection[tool]; selected {
- if err = manageUI(builder.Builders[tool](conf, hclParser)); err != nil {
+ if err = manageUI(ctx, builder.Builders[tool](conf, hclParser)); err != nil {
return err
}
}
@@ -221,10 +222,10 @@ func toolUI(conf *config.Config, hclParser *hclparse.Parser) error {
return nil
}
-func manageUI(versionManager versionmanager.VersionManager) error {
+func manageUI(ctx context.Context, versionManager versionmanager.VersionManager) error {
installed := versionManager.LocalSet()
- remoteVersions, err := versionManager.ListRemote(true)
+ remoteVersions, err := versionManager.ListRemote(ctx, true)
if err != nil {
return err
}
@@ -286,7 +287,7 @@ func manageUI(versionManager versionmanager.VersionManager) error {
return nil
}
- return versionManager.InstallMultiple(toInstall)
+ return versionManager.InstallMultiple(ctx, toInstall)
}
func uninstallUI(versionManager versionmanager.VersionManager) error {
diff --git a/config/config.go b/config/config.go
index c7c56072..2857c4ee 100644
--- a/config/config.go
+++ b/config/config.go
@@ -35,6 +35,7 @@ import (
)
const (
+ defaultDirName = ".tenv"
githubActionsEnvName = "GITHUB_ACTIONS"
archEnvName = "ARCH"
@@ -132,10 +133,10 @@ type Config struct {
ForceRemote bool
GithubActions bool
GithubToken string
- NoInstall bool
remoteConfLoaded bool
RemoteConfPath string
RootPath string
+ SkipInstall bool
SkipSignature bool
Tf RemoteConfig
TfKeyPath string
@@ -145,6 +146,25 @@ type Config struct {
UserPath string
}
+func DefaultConfig() (Config, error) {
+ userPath, err := os.UserHomeDir()
+ if err != nil {
+ return Config{}, err
+ }
+
+ return Config{
+ Arch: runtime.GOARCH,
+ Atmos: makeDefaultRemoteConfig(defaultAtmosGithubURL, baseGithubURL),
+ remoteConfLoaded: true,
+ RootPath: filepath.Join(userPath, defaultDirName),
+ SkipInstall: true,
+ Tf: makeDefaultRemoteConfig(defaultHashicorpURL, defaultHashicorpURL),
+ Tg: makeDefaultRemoteConfig(defaultTerragruntGithubURL, baseGithubURL),
+ Tofu: makeDefaultRemoteConfig(DefaultTofuGithubURL, baseGithubURL),
+ UserPath: userPath,
+ }, nil
+}
+
func InitConfigFromEnv() (Config, error) {
userPath, err := os.UserHomeDir()
if err != nil {
@@ -168,7 +188,7 @@ func InitConfigFromEnv() (Config, error) {
rootPath := configutils.GetenvFallback(tenvRootPathEnvName, tofuRootPathEnvName, tfRootPathEnvName)
if rootPath == "" {
- rootPath = filepath.Join(userPath, ".tenv")
+ rootPath = filepath.Join(userPath, defaultDirName)
}
quiet, err := configutils.GetenvBoolFallback(false, tenvQuietEnvName)
@@ -188,9 +208,9 @@ func InitConfigFromEnv() (Config, error) {
ForceRemote: forceRemote,
GithubActions: gha,
GithubToken: configutils.GetenvFallback(tenvTokenEnvName, tofuTokenEnvName),
- NoInstall: !autoInstall,
RemoteConfPath: os.Getenv(tenvRemoteConfEnvName),
RootPath: rootPath,
+ SkipInstall: !autoInstall,
Tf: makeRemoteConfig(TfRemoteURLEnvName, tfListURLEnvName, tfInstallModeEnvName, tfListModeEnvName, defaultHashicorpURL, defaultHashicorpURL),
TfKeyPath: os.Getenv(tfHashicorpPGPKeyEnvName),
Tg: makeRemoteConfig(TgRemoteURLEnvName, tgListURLEnvName, tgInstallModeEnvName, tgListModeEnvName, defaultTerragruntGithubURL, baseGithubURL),
@@ -228,9 +248,9 @@ func (conf *Config) InitDisplayer(proxyCall bool) {
func (conf *Config) InitInstall(forceInstall bool, forceNoInstall bool) {
switch {
case forceNoInstall: // higher priority to --no-install
- conf.NoInstall = true
+ conf.SkipInstall = true
case forceInstall:
- conf.NoInstall = false
+ conf.SkipInstall = false
}
}
diff --git a/config/remote.go b/config/remote.go
index 0c0b7055..21a20998 100644
--- a/config/remote.go
+++ b/config/remote.go
@@ -56,6 +56,12 @@ type RemoteConfig struct {
RemoteURLEnv string // value from env
}
+func makeDefaultRemoteConfig(defaultURL string, defaultBaseURL string) RemoteConfig {
+ return RemoteConfig{
+ defaultBaseURL: defaultBaseURL, defaultURL: defaultURL, Data: map[string]string{},
+ }
+}
+
func makeRemoteConfig(remoteURLEnvName string, listURLEnvName string, installModeEnvName string, listModeEnvName string, defaultURL string, defaultBaseURL string) RemoteConfig {
return RemoteConfig{
defaultBaseURL: defaultBaseURL, defaultURL: defaultURL, installMode: os.Getenv(installModeEnvName), listMode: os.Getenv(listModeEnvName),
diff --git a/go.mod b/go.mod
index 81fc39c9..60fe6a40 100644
--- a/go.mod
+++ b/go.mod
@@ -1,13 +1,15 @@
module github.com/tofuutils/tenv/v3
-go 1.21
+go 1.23
+
+toolchain go1.23.1
require (
github.com/BurntSushi/toml v1.4.0
github.com/ProtonMail/gopenpgp/v2 v2.7.5
- github.com/PuerkitoBio/goquery v1.9.2
- github.com/charmbracelet/bubbles v0.19.0
- github.com/charmbracelet/bubbletea v0.27.1
+ github.com/PuerkitoBio/goquery v1.10.0
+ github.com/charmbracelet/bubbles v0.20.0
+ github.com/charmbracelet/bubbletea v1.1.0
github.com/charmbracelet/lipgloss v0.13.0
github.com/fatih/color v1.17.0
github.com/hashicorp/go-hclog v1.6.3
@@ -27,11 +29,9 @@ require (
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
- github.com/charmbracelet/x/ansi v0.1.4 // indirect
- github.com/charmbracelet/x/input v0.1.3 // indirect
- github.com/charmbracelet/x/term v0.1.1 // indirect
- github.com/charmbracelet/x/windows v0.1.2 // indirect
- github.com/cloudflare/circl v1.3.9 // indirect
+ github.com/charmbracelet/x/ansi v0.2.3 // indirect
+ github.com/charmbracelet/x/term v0.2.0 // indirect
+ github.com/cloudflare/circl v1.4.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/go-test/deep v1.1.0 // indirect
@@ -50,12 +50,11 @@ require (
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sahilm/fuzzy v0.1.1 // indirect
github.com/stretchr/testify v1.8.4 // indirect
- github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
- golang.org/x/crypto v0.26.0 // indirect
- golang.org/x/mod v0.20.0 // indirect
- golang.org/x/net v0.28.0 // indirect
+ golang.org/x/crypto v0.27.0 // indirect
+ golang.org/x/mod v0.21.0 // indirect
+ golang.org/x/net v0.29.0 // indirect
golang.org/x/sync v0.8.0 // indirect
- golang.org/x/sys v0.24.0 // indirect
- golang.org/x/text v0.17.0 // indirect
+ golang.org/x/sys v0.25.0 // indirect
+ golang.org/x/text v0.18.0 // indirect
golang.org/x/tools v0.24.0 // indirect
)
diff --git a/go.sum b/go.sum
index b387a237..d112c492 100644
--- a/go.sum
+++ b/go.sum
@@ -7,8 +7,8 @@ github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ek
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
github.com/ProtonMail/gopenpgp/v2 v2.7.5 h1:STOY3vgES59gNgoOt2w0nyHBjKViB/qSg7NjbQWPJkA=
github.com/ProtonMail/gopenpgp/v2 v2.7.5/go.mod h1:IhkNEDaxec6NyzSI0PlxapinnwPVIESk8/76da3Ct3g=
-github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
-github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
+github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4=
+github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4=
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
@@ -20,23 +20,19 @@ github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
-github.com/charmbracelet/bubbles v0.19.0 h1:gKZkKXPP6GlDk6EcfujDK19PCQqRjaJZQ7QRERx1UF0=
-github.com/charmbracelet/bubbles v0.19.0/go.mod h1:WILteEqZ+krG5c3ntGEMeG99nCupcuIk7V0/zOP0tOA=
-github.com/charmbracelet/bubbletea v0.27.1 h1:/yhaJKX52pxG4jZVKCNWj/oq0QouPdXycriDRA6m6r8=
-github.com/charmbracelet/bubbletea v0.27.1/go.mod h1:xc4gm5yv+7tbniEvQ0naiG9P3fzYhk16cTgDZQQW6YE=
+github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
+github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
+github.com/charmbracelet/bubbletea v1.1.0 h1:FjAl9eAL3HBCHenhz/ZPjkKdScmaS5SK69JAK2YJK9c=
+github.com/charmbracelet/bubbletea v1.1.0/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4=
github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw=
github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY=
-github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM=
-github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
-github.com/charmbracelet/x/input v0.1.3 h1:oy4TMhyGQsYs/WWJwu1ELUMFnjiUAXwtDf048fHbCkg=
-github.com/charmbracelet/x/input v0.1.3/go.mod h1:1gaCOyw1KI9e2j00j/BBZ4ErzRZqa05w0Ghn83yIhKU=
-github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI=
-github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw=
-github.com/charmbracelet/x/windows v0.1.2 h1:Iumiwq2G+BRmgoayww/qfcvof7W/3uLoelhxojXlRWg=
-github.com/charmbracelet/x/windows v0.1.2/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ=
+github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY=
+github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
+github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0=
+github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
-github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE=
-github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
+github.com/cloudflare/circl v1.4.0 h1:BV7h5MgrktNzytKmWjpOtdYrf0lkkbF8YMlBGPhJQrY=
+github.com/cloudflare/circl v1.4.0/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -104,8 +100,6 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
-github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zclconf/go-cty v1.15.0 h1:tTCRWxsexYUmtt/wVxgDClUe+uQusuI443uL6e+5sXQ=
github.com/zclconf/go-cty v1.15.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
@@ -115,14 +109,12 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
-golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
-golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
-golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
-golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
+golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
+golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
-golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
+golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
@@ -130,8 +122,8 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
-golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
-golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
+golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
+golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -154,8 +146,8 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
-golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
+golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
@@ -169,8 +161,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
-golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
+golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
diff --git a/pkg/cmdproxy/proxy.go b/pkg/cmdproxy/proxy.go
index 247789fa..25beb9ed 100644
--- a/pkg/cmdproxy/proxy.go
+++ b/pkg/cmdproxy/proxy.go
@@ -32,24 +32,23 @@ import (
var errDelimiter = errors.New("key and value should not contains delimiter")
-func Run(execPath string, cmdArgs []string, gha bool) {
+// Always call os.Exit.
+func Run(cmd *exec.Cmd, gha bool) {
exitCode := 0
defer func() {
os.Exit(exitCode)
}()
- // proxy to selected version
- cmd := exec.Command(execPath, cmdArgs...)
- done, err := initIO(cmd, execPath, &exitCode, gha)
+ done, err := initIO(cmd, &exitCode, gha)
if err != nil {
- exitWithErrorMsg(execPath, err, &exitCode)
+ exitWithErrorMsg(cmd.Path, err, &exitCode)
return
}
defer done()
if err = cmd.Start(); err != nil {
- exitWithErrorMsg(execPath, err, &exitCode)
+ exitWithErrorMsg(cmd.Path, err, &exitCode)
return
}
@@ -65,18 +64,18 @@ func Run(execPath string, cmdArgs []string, gha bool) {
return
}
- exitWithErrorMsg(execPath, err, &exitCode)
+ exitWithErrorMsg(cmd.Path, err, &exitCode)
}
}
-func exitWithErrorMsg(execName string, err error, pExitCode *int) {
- fmt.Println("Failure during", execName, "call :", err) //nolint
+func exitWithErrorMsg(execPath string, err error, pExitCode *int) {
+ fmt.Println("Failure during", execPath, "call :", err) //nolint
if *pExitCode == 0 {
*pExitCode = 1
}
}
-func initIO(cmd *exec.Cmd, execName string, pExitCode *int, gha bool) (func(), error) {
+func initIO(cmd *exec.Cmd, pExitCode *int, gha bool) (func(), error) {
cmd.Stdin = os.Stdin
if !gha {
cmd.Stderr = os.Stderr
@@ -100,27 +99,27 @@ func initIO(cmd *exec.Cmd, execName string, pExitCode *int, gha bool) (func(), e
err = writeMultiline(outputFile, "stderr", errBuffer.String())
if err != nil {
- exitWithErrorMsg(execName, err, pExitCode)
+ exitWithErrorMsg(cmd.Path, err, pExitCode)
return
}
if err = writeMultiline(outputFile, "stdout", outBuffer.String()); err != nil {
- exitWithErrorMsg(execName, err, pExitCode)
+ exitWithErrorMsg(cmd.Path, err, pExitCode)
return
}
exitCode := *pExitCode
if err = writeMultiline(outputFile, "exitcode", strconv.Itoa(exitCode)); err != nil {
- exitWithErrorMsg(execName, err, pExitCode)
+ exitWithErrorMsg(cmd.Path, err, pExitCode)
return
}
if exitCode != 0 && exitCode != 2 {
err = fmt.Errorf("exited with code %d", exitCode)
- exitWithErrorMsg(execName, err, pExitCode)
+ exitWithErrorMsg(cmd.Path, err, pExitCode)
}
}, nil
}
diff --git a/pkg/download/download.go b/pkg/download/download.go
index 37c74b61..48831f8c 100644
--- a/pkg/download/download.go
+++ b/pkg/download/download.go
@@ -19,13 +19,14 @@
package download
import (
+ "context"
"encoding/json"
"io"
"net/http"
"net/url"
)
-type RequestOption = func(*http.Request) *http.Request
+type RequestOption = func(*http.Request)
func ApplyUrlTranformer(urlTransformer func(string) (string, error), baseURLs ...string) ([]string, error) {
transformedURLs := make([]string, 0, len(baseURLs))
@@ -41,16 +42,16 @@ func ApplyUrlTranformer(urlTransformer func(string) (string, error), baseURLs ..
return transformedURLs, nil
}
-func Bytes(url string, display func(string), requestOptions ...RequestOption) ([]byte, error) {
+func Bytes(ctx context.Context, url string, display func(string), requestOptions ...RequestOption) ([]byte, error) {
display("Downloading " + url)
- request, err := http.NewRequest(http.MethodGet, url, http.NoBody)
+ request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
if err != nil {
return nil, err
}
for _, option := range requestOptions {
- request = option(request)
+ option(request)
}
response, err := http.DefaultClient.Do(request)
@@ -62,8 +63,8 @@ func Bytes(url string, display func(string), requestOptions ...RequestOption) ([
return io.ReadAll(response.Body)
}
-func JSON(url string, display func(string), requestOptions ...RequestOption) (any, error) {
- data, err := Bytes(url, display, requestOptions...)
+func JSON(ctx context.Context, url string, display func(string), requestOptions ...RequestOption) (any, error) {
+ data, err := Bytes(ctx, url, display, requestOptions...)
if err != nil {
return nil, err
}
@@ -98,10 +99,8 @@ func UrlTranformer(rewriteRule []string) func(string) (string, error) {
}
func WithBasicAuth(username string, password string) RequestOption {
- return func(r *http.Request) *http.Request {
+ return func(r *http.Request) {
r.SetBasicAuth(username, password)
-
- return r
}
}
diff --git a/pkg/github/github.go b/pkg/github/github.go
index 6c532d07..354d9043 100644
--- a/pkg/github/github.go
+++ b/pkg/github/github.go
@@ -19,6 +19,7 @@
package github
import (
+ "context"
"errors"
"net/http"
"net/url"
@@ -38,7 +39,7 @@ const (
var errContinue = errors.New("continue")
-func AssetDownloadURL(tag string, searchedAssetNames []string, githubReleaseURL string, githubToken string, display func(string)) ([]string, error) {
+func AssetDownloadURL(ctx context.Context, tag string, searchedAssetNames []string, githubReleaseURL string, githubToken string, display func(string)) ([]string, error) {
releaseUrl, err := url.JoinPath(githubReleaseURL, "tags", tag) //nolint
if err != nil {
return nil, err
@@ -47,7 +48,7 @@ func AssetDownloadURL(tag string, searchedAssetNames []string, githubReleaseURL
display(apimsg.MsgFetchRelease + releaseUrl)
authorizationHeader := buildAuthorizationHeader(githubToken)
- value, err := apiGetRequest(releaseUrl, authorizationHeader)
+ value, err := apiGetRequest(ctx, releaseUrl, authorizationHeader)
if err != nil {
return nil, err
}
@@ -69,7 +70,7 @@ func AssetDownloadURL(tag string, searchedAssetNames []string, githubReleaseURL
baseAssetsURL += pageQuery
for {
assetsURL := baseAssetsURL + strconv.Itoa(page)
- value, err = apiGetRequest(assetsURL, authorizationHeader)
+ value, err = apiGetRequest(ctx, assetsURL, authorizationHeader)
if err != nil {
return nil, err
}
@@ -88,7 +89,7 @@ func AssetDownloadURL(tag string, searchedAssetNames []string, githubReleaseURL
}
}
-func ListReleases(githubReleaseURL string, githubToken string) ([]string, error) {
+func ListReleases(ctx context.Context, githubReleaseURL string, githubToken string) ([]string, error) {
basePageURL := githubReleaseURL + pageQuery
authorizationHeader := buildAuthorizationHeader(githubToken)
@@ -96,7 +97,7 @@ func ListReleases(githubReleaseURL string, githubToken string) ([]string, error)
var releases []string
for {
pageURL := basePageURL + strconv.Itoa(page)
- value, err := apiGetRequest(pageURL, authorizationHeader)
+ value, err := apiGetRequest(ctx, pageURL, authorizationHeader)
if err != nil {
return nil, err
}
@@ -111,15 +112,13 @@ func ListReleases(githubReleaseURL string, githubToken string) ([]string, error)
}
}
-func apiGetRequest(callURL string, authorizationHeader string) (any, error) {
- return download.JSON(callURL, download.NoDisplay, func(request *http.Request) *http.Request {
+func apiGetRequest(ctx context.Context, callURL string, authorizationHeader string) (any, error) {
+ return download.JSON(ctx, callURL, download.NoDisplay, func(request *http.Request) {
request.Header.Set("Accept", "application/vnd.github+json")
if authorizationHeader != "" {
request.Header.Set("Authorization", authorizationHeader)
}
request.Header.Set("X-GitHub-Api-Version", "2022-11-28")
-
- return request
})
}
diff --git a/pkg/htmlquery/html.go b/pkg/htmlquery/html.go
index 79c9ff6a..ee4edec0 100644
--- a/pkg/htmlquery/html.go
+++ b/pkg/htmlquery/html.go
@@ -20,6 +20,7 @@ package htmlquery
import (
"bytes"
+ "context"
"strings"
"github.com/PuerkitoBio/goquery"
@@ -27,8 +28,8 @@ import (
"github.com/tofuutils/tenv/v3/pkg/download"
)
-func Request(callURL string, selector string, extractor func(*goquery.Selection) string, ro ...download.RequestOption) ([]string, error) {
- data, err := download.Bytes(callURL, download.NoDisplay, ro...)
+func Request(ctx context.Context, callURL string, selector string, extractor func(*goquery.Selection) string, ro ...download.RequestOption) ([]string, error) {
+ data, err := download.Bytes(ctx, callURL, download.NoDisplay, ro...)
if err != nil {
return nil, err
}
diff --git a/versionmanager/manager.go b/versionmanager/manager.go
index 29927c9e..5dbcb5a8 100644
--- a/versionmanager/manager.go
+++ b/versionmanager/manager.go
@@ -19,6 +19,7 @@
package versionmanager
import (
+ "context"
"errors"
"io/fs"
"os"
@@ -48,8 +49,8 @@ var (
)
type ReleaseInfoRetriever interface {
- InstallRelease(version string, targetPath string) error
- ListReleases() ([]string, error)
+ InstallRelease(ctx context.Context, version string, targetPath string) error
+ ListReleases(ctx context.Context) ([]string, error)
}
type DatedVersion struct {
@@ -73,7 +74,7 @@ func Make(conf *config.Config, constraintEnvName string, folderName string, iacE
}
// Detect version (resolve and evaluate, can install depending on auto install env var).
-func (m VersionManager) Detect(proxyCall bool) (string, error) {
+func (m VersionManager) Detect(ctx context.Context, proxyCall bool) (string, error) {
configVersion, err := m.Resolve(semantic.LatestAllowedKey)
if err != nil {
m.conf.Displayer.Flush(proxyCall)
@@ -81,15 +82,15 @@ func (m VersionManager) Detect(proxyCall bool) (string, error) {
return "", err
}
- return m.Evaluate(configVersion, proxyCall)
+ return m.Evaluate(ctx, configVersion, proxyCall)
}
// Evaluate version resolution strategy or version constraint (can install depending on auto install env var).
-func (m VersionManager) Evaluate(requestedVersion string, proxyCall bool) (string, error) {
+func (m VersionManager) Evaluate(ctx context.Context, requestedVersion string, proxyCall bool) (string, error) {
parsedVersion, err := version.NewVersion(requestedVersion)
if err == nil {
cleanedVersion := parsedVersion.String() // use a parsable version
- if m.conf.NoInstall {
+ if m.conf.SkipInstall {
_, installed, err := m.checkVersionInstallation("", cleanedVersion)
if err != nil {
return "", err
@@ -103,7 +104,7 @@ func (m VersionManager) Evaluate(requestedVersion string, proxyCall bool) (strin
return cleanedVersion, nil
}
- return cleanedVersion, m.installSpecificVersion(cleanedVersion, proxyCall)
+ return cleanedVersion, m.installSpecificVersion(ctx, cleanedVersion, proxyCall)
}
predicateInfo, err := semantic.ParsePredicate(requestedVersion, m.FolderName, m, m.iacExts, m.conf)
@@ -140,13 +141,13 @@ func (m VersionManager) Evaluate(requestedVersion string, proxyCall bool) (strin
m.conf.Displayer.Display("No compatible version found locally, search a remote one...")
}
- return m.searchInstallRemote(predicateInfo, m.conf.NoInstall, proxyCall)
+ return m.searchInstallRemote(ctx, predicateInfo, m.conf.SkipInstall, proxyCall)
}
-func (m VersionManager) Install(requestedVersion string) error {
+func (m VersionManager) Install(ctx context.Context, requestedVersion string) error {
parsedVersion, err := version.NewVersion(requestedVersion)
if err == nil {
- return m.installSpecificVersion(parsedVersion.String(), false) // use a parsable version
+ return m.installSpecificVersion(ctx, parsedVersion.String(), false) // use a parsable version
}
predicateInfo, err := semantic.ParsePredicate(requestedVersion, m.FolderName, m, m.iacExts, m.conf)
@@ -155,12 +156,12 @@ func (m VersionManager) Install(requestedVersion string) error {
}
// noInstall is set to false to force install regardless of conf
- _, err = m.searchInstallRemote(predicateInfo, false, false)
+ _, err = m.searchInstallRemote(ctx, predicateInfo, false, false)
return err
}
-func (m VersionManager) InstallMultiple(versions []string) error {
+func (m VersionManager) InstallMultiple(ctx context.Context, versions []string) error {
installPath, err := m.InstallPath()
if err != nil {
return err
@@ -172,7 +173,7 @@ func (m VersionManager) InstallMultiple(versions []string) error {
defer deleteLock()
for _, version := range versions {
- if err = m.installSpecificVersionWithoutLock(installPath, version, false); err != nil {
+ if err = m.installSpecificVersionWithoutLock(ctx, installPath, version, false); err != nil {
return err
}
}
@@ -210,8 +211,8 @@ func (m VersionManager) ListLocal(reverseOrder bool) ([]DatedVersion, error) {
return datedVersions, nil
}
-func (m VersionManager) ListRemote(reverseOrder bool) ([]string, error) {
- versions, err := m.retriever.ListReleases()
+func (m VersionManager) ListRemote(ctx context.Context, reverseOrder bool) ([]string, error) {
+ versions, err := m.retriever.ListReleases(ctx)
if err != nil {
return nil, err
}
@@ -384,8 +385,8 @@ func (m VersionManager) UninstallMultiple(versions []string) error {
return nil
}
-func (m VersionManager) Use(requestedVersion string, workingDir bool) error {
- detectedVersion, err := m.Evaluate(requestedVersion, false)
+func (m VersionManager) Use(ctx context.Context, requestedVersion string, workingDir bool) error {
+ detectedVersion, err := m.Evaluate(ctx, requestedVersion, false)
if err != nil {
if err != ErrNoCompatibleLocally {
return err
@@ -454,7 +455,7 @@ func (m VersionManager) innerListLocal(installPath string, reverseOrder bool) ([
return versions, nil
}
-func (m VersionManager) installSpecificVersion(version string, proxyCall bool) error {
+func (m VersionManager) installSpecificVersion(ctx context.Context, version string, proxyCall bool) error {
if version == "" {
m.conf.Displayer.Flush(proxyCall)
@@ -478,10 +479,10 @@ func (m VersionManager) installSpecificVersion(version string, proxyCall bool) e
defer disableExit()
defer deleteLock()
- return m.installSpecificVersionWithoutLock(installPath, version, proxyCall)
+ return m.installSpecificVersionWithoutLock(ctx, installPath, version, proxyCall)
}
-func (m VersionManager) installSpecificVersionWithoutLock(installPath string, version string, proxyCall bool) error {
+func (m VersionManager) installSpecificVersionWithoutLock(ctx context.Context, installPath string, version string, proxyCall bool) error {
// second check with lock to ensure there is no ongoing install
_, installed, err := m.checkVersionInstallation(installPath, version)
if err != nil {
@@ -498,7 +499,7 @@ func (m VersionManager) installSpecificVersionWithoutLock(installPath string, ve
m.conf.Displayer.Flush(false)
m.conf.Displayer.Display(loghelper.Concat("Installing ", m.FolderName, " ", version))
- err = m.retriever.InstallRelease(version, filepath.Join(installPath, version))
+ err = m.retriever.InstallRelease(ctx, version, filepath.Join(installPath, version))
if err == nil {
m.conf.Displayer.Display(loghelper.Concat("Installation of ", m.FolderName, " ", version, " successful"))
}
@@ -506,8 +507,8 @@ func (m VersionManager) installSpecificVersionWithoutLock(installPath string, ve
return err
}
-func (m VersionManager) searchInstallRemote(predicateInfo types.PredicateInfo, noInstall bool, proxyCall bool) (string, error) {
- versions, err := m.ListRemote(predicateInfo.ReverseOrder)
+func (m VersionManager) searchInstallRemote(ctx context.Context, predicateInfo types.PredicateInfo, noInstall bool, proxyCall bool) (string, error) {
+ versions, err := m.ListRemote(ctx, predicateInfo.ReverseOrder)
if err != nil {
m.conf.Displayer.Flush(proxyCall)
@@ -521,7 +522,7 @@ func (m VersionManager) searchInstallRemote(predicateInfo types.PredicateInfo, n
return version, m.autoInstallDisabledMsg(version)
}
- return version, m.installSpecificVersion(version, proxyCall)
+ return version, m.installSpecificVersion(ctx, version, proxyCall)
}
}
m.conf.Displayer.Flush(proxyCall)
diff --git a/versionmanager/proxy/agnostic.go b/versionmanager/proxy/agnostic.go
index 39f27de0..21fd4b54 100644
--- a/versionmanager/proxy/agnostic.go
+++ b/versionmanager/proxy/agnostic.go
@@ -19,18 +19,23 @@
package proxy
import (
+ "context"
"fmt"
"os"
+ "os/exec"
"github.com/hashicorp/hcl/v2/hclparse"
"github.com/tofuutils/tenv/v3/config"
"github.com/tofuutils/tenv/v3/config/cmdconst"
+ cmdproxy "github.com/tofuutils/tenv/v3/pkg/cmdproxy"
"github.com/tofuutils/tenv/v3/versionmanager/builder"
)
+// Always call os.Exit.
func ExecAgnostic(conf *config.Config, hclParser *hclparse.Parser, cmdArgs []string) {
conf.InitDisplayer(true)
+ ctx := context.Background()
manager := builder.BuildTofuManager(conf, hclParser)
detectedVersion, err := manager.ResolveWithVersionFiles()
if err != nil {
@@ -60,11 +65,13 @@ func ExecAgnostic(conf *config.Config, hclParser *hclparse.Parser, cmdArgs []str
os.Exit(1)
}
- detectedVersion, err = manager.Evaluate(detectedVersion, true)
+ detectedVersion, err = manager.Evaluate(ctx, detectedVersion, true)
if err != nil {
fmt.Println("Failed to evaluate the requested version in a specific version allowing to call", execName, ":", err) //nolint
os.Exit(1)
}
- RunCmd(installPath, detectedVersion, execName, cmdArgs, conf.GithubActions, conf.Displayer)
+ execPath := ExecPath(installPath, detectedVersion, execName, conf.Displayer)
+
+ cmdproxy.Run(exec.CommandContext(ctx, execPath, cmdArgs...), conf.GithubActions)
}
diff --git a/versionmanager/proxy/proxy.go b/versionmanager/proxy/proxy.go
index b362ff2d..8657eed9 100644
--- a/versionmanager/proxy/proxy.go
+++ b/versionmanager/proxy/proxy.go
@@ -19,9 +19,11 @@
package proxy
import (
+ "context"
"errors"
"fmt"
"os"
+ "os/exec"
"path/filepath"
"github.com/hashicorp/hcl/v2/hclparse"
@@ -35,10 +37,12 @@ import (
var errDelimiter = errors.New("key and value should not contains delimiter")
+// Always call os.Exit.
func Exec(conf *config.Config, builderFunc builder.BuilderFunc, hclParser *hclparse.Parser, execName string, cmdArgs []string) {
conf.InitDisplayer(true)
+ ctx := context.Background()
versionManager := builderFunc(conf, hclParser)
- detectedVersion, err := versionManager.Detect(true)
+ detectedVersion, err := versionManager.Detect(ctx, true)
if err != nil {
fmt.Println("Failed to detect a version allowing to call", execName, ":", err) //nolint
os.Exit(1)
@@ -50,13 +54,14 @@ func Exec(conf *config.Config, builderFunc builder.BuilderFunc, hclParser *hclpa
os.Exit(1)
}
- RunCmd(installPath, detectedVersion, execName, cmdArgs, conf.GithubActions, conf.Displayer)
-}
+ execPath := ExecPath(installPath, detectedVersion, execName, conf.Displayer)
-func RunCmd(installPath string, detectedVersion string, execName string, cmdArgs []string, gha bool, displayer loghelper.Displayer) {
- versionPath := filepath.Join(installPath, detectedVersion)
+ cmdproxy.Run(exec.CommandContext(ctx, execPath, cmdArgs...), conf.GithubActions)
+}
+func ExecPath(installPath string, version string, execName string, displayer loghelper.Displayer) string {
+ versionPath := filepath.Join(installPath, version)
lastuse.WriteNow(versionPath, displayer)
- cmdproxy.Run(filepath.Join(versionPath, execName), cmdArgs, gha)
+ return filepath.Join(versionPath, execName)
}
diff --git a/versionmanager/retriever/atmos/atmosretriever.go b/versionmanager/retriever/atmos/atmosretriever.go
index 40e5137f..fecc52a2 100644
--- a/versionmanager/retriever/atmos/atmosretriever.go
+++ b/versionmanager/retriever/atmos/atmosretriever.go
@@ -19,6 +19,7 @@
package atmosretriever
import (
+ "context"
"net/url"
"os"
"path/filepath"
@@ -50,7 +51,7 @@ func Make(conf *config.Config) AtmosRetriever {
return AtmosRetriever{conf: conf}
}
-func (r AtmosRetriever) InstallRelease(versionStr string, targetPath string) error {
+func (r AtmosRetriever) InstallRelease(ctx context.Context, versionStr string, targetPath string) error {
err := r.conf.InitRemoteConf()
if err != nil {
return err
@@ -80,7 +81,7 @@ func (r AtmosRetriever) InstallRelease(versionStr string, targetPath string) err
assetURLs, err = htmlretriever.BuildAssetURLs(baseAssetURL, fileName, shaFileName)
case config.ModeAPI:
- assetURLs, err = github.AssetDownloadURL(tag, []string{fileName, shaFileName}, r.conf.Atmos.GetRemoteURL(), r.conf.GithubToken, r.conf.Displayer.Display)
+ assetURLs, err = github.AssetDownloadURL(ctx, tag, []string{fileName, shaFileName}, r.conf.Atmos.GetRemoteURL(), r.conf.GithubToken, r.conf.Displayer.Display)
default:
return config.ErrInstallMode
}
@@ -95,12 +96,12 @@ func (r AtmosRetriever) InstallRelease(versionStr string, targetPath string) err
}
ro := config.GetBasicAuthOption(config.AtmosRemoteUserEnvName, config.AtmosRemotePassEnvName)
- data, err := download.Bytes(assetURLs[0], r.conf.Displayer.Display, ro...)
+ data, err := download.Bytes(ctx, assetURLs[0], r.conf.Displayer.Display, ro...)
if err != nil {
return err
}
- dataSums, err := download.Bytes(assetURLs[1], r.conf.Displayer.Display, ro...)
+ dataSums, err := download.Bytes(ctx, assetURLs[1], r.conf.Displayer.Display, ro...)
if err != nil {
return err
}
@@ -117,7 +118,7 @@ func (r AtmosRetriever) InstallRelease(versionStr string, targetPath string) err
return os.WriteFile(filepath.Join(targetPath, winbin.GetBinaryName(cmdconst.AtmosName)), data, 0o755)
}
-func (r AtmosRetriever) ListReleases() ([]string, error) {
+func (r AtmosRetriever) ListReleases(ctx context.Context) ([]string, error) {
err := r.conf.InitRemoteConf()
if err != nil {
return nil, err
@@ -135,11 +136,11 @@ func (r AtmosRetriever) ListReleases() ([]string, error) {
r.conf.Displayer.Display(apimsg.MsgFetchAllReleases + baseURL)
- return htmlretriever.ListReleases(baseURL, r.conf.Atmos.Data, ro)
+ return htmlretriever.ListReleases(ctx, baseURL, r.conf.Atmos.Data, ro)
case config.ModeAPI:
r.conf.Displayer.Display(apimsg.MsgFetchAllReleases + listURL)
- return github.ListReleases(listURL, r.conf.GithubToken)
+ return github.ListReleases(ctx, listURL, r.conf.GithubToken)
default:
return nil, config.ErrListMode
}
diff --git a/versionmanager/retriever/html/htmlretriever.go b/versionmanager/retriever/html/htmlretriever.go
index 4bc71b51..ee733761 100644
--- a/versionmanager/retriever/html/htmlretriever.go
+++ b/versionmanager/retriever/html/htmlretriever.go
@@ -19,6 +19,7 @@
package htmlretriever
import (
+ "context"
"net/url"
"github.com/PuerkitoBio/goquery"
@@ -37,12 +38,12 @@ func BuildAssetURLs(baseAssetURL string, assetNames ...string) ([]string, error)
return download.ApplyUrlTranformer(joinTransformer, assetNames...)
}
-func ListReleases(baseURL string, remoteConf map[string]string, ro []download.RequestOption) ([]string, error) {
+func ListReleases(ctx context.Context, baseURL string, remoteConf map[string]string, ro []download.RequestOption) ([]string, error) {
selector := config.MapGetDefault(remoteConf, "selector", "a")
extractor := htmlquery.SelectionExtractor(config.MapGetDefault(remoteConf, "part", "href"))
versionExtractor := func(s *goquery.Selection) string {
return versionfinder.Find(extractor(s))
}
- return htmlquery.Request(baseURL, selector, versionExtractor, ro...)
+ return htmlquery.Request(ctx, baseURL, selector, versionExtractor, ro...)
}
diff --git a/versionmanager/retriever/terraform/terraformretriever.go b/versionmanager/retriever/terraform/terraformretriever.go
index 4b4984a4..c7d4acbd 100644
--- a/versionmanager/retriever/terraform/terraformretriever.go
+++ b/versionmanager/retriever/terraform/terraformretriever.go
@@ -19,6 +19,7 @@
package terraformretriever
import (
+ "context"
"net/url"
"os"
"runtime"
@@ -54,7 +55,7 @@ func Make(conf *config.Config) TerraformRetriever {
return TerraformRetriever{conf: conf}
}
-func (r TerraformRetriever) InstallRelease(version string, targetPath string) error {
+func (r TerraformRetriever) InstallRelease(ctx context.Context, version string, targetPath string) error {
err := r.conf.InitRemoteConf()
if err != nil {
return err
@@ -94,7 +95,7 @@ func (r TerraformRetriever) InstallRelease(version string, targetPath string) er
r.conf.Displayer.Display(apimsg.MsgFetchRelease + versionUrl)
- value, err := download.JSON(versionUrl, download.NoDisplay, ro...)
+ value, err := download.JSON(ctx, versionUrl, download.NoDisplay, ro...)
if err != nil {
return err
}
@@ -124,19 +125,19 @@ func (r TerraformRetriever) InstallRelease(version string, targetPath string) er
return err
}
- data, err := download.Bytes(assetURLs[0], r.conf.Displayer.Display, ro...)
+ data, err := download.Bytes(ctx, assetURLs[0], r.conf.Displayer.Display, ro...)
if err != nil {
return err
}
- if err = r.checkSumAndSig(fileName, data, assetURLs[1], assetURLs[2], ro); err != nil {
+ if err = r.checkSumAndSig(ctx, fileName, data, assetURLs[1], assetURLs[2], ro); err != nil {
return err
}
return zip.UnzipToDir(data, targetPath, pathfilter.NameEqual(winbin.GetBinaryName(cmdconst.TerraformName)))
}
-func (r TerraformRetriever) ListReleases() ([]string, error) {
+func (r TerraformRetriever) ListReleases(ctx context.Context) ([]string, error) {
err := r.conf.InitRemoteConf()
if err != nil {
return nil, err
@@ -153,7 +154,7 @@ func (r TerraformRetriever) ListReleases() ([]string, error) {
case config.ListModeHTML:
r.conf.Displayer.Display(apimsg.MsgFetchAllReleases + baseURL)
- return htmlretriever.ListReleases(baseURL, r.conf.Tf.Data, ro)
+ return htmlretriever.ListReleases(ctx, baseURL, r.conf.Tf.Data, ro)
case config.ModeAPI:
releasesURL, err := url.JoinPath(baseURL, indexJson) //nolint
if err != nil {
@@ -162,7 +163,7 @@ func (r TerraformRetriever) ListReleases() ([]string, error) {
r.conf.Displayer.Display(apimsg.MsgFetchAllReleases + releasesURL)
- value, err := download.JSON(releasesURL, download.NoDisplay, ro...)
+ value, err := download.JSON(ctx, releasesURL, download.NoDisplay, ro...)
if err != nil {
return nil, err
}
@@ -173,8 +174,8 @@ func (r TerraformRetriever) ListReleases() ([]string, error) {
}
}
-func (r TerraformRetriever) checkSumAndSig(fileName string, data []byte, downloadSumsURL string, downloadSumsSigURL string, ro []download.RequestOption) error {
- dataSums, err := download.Bytes(downloadSumsURL, r.conf.Displayer.Display, ro...)
+func (r TerraformRetriever) checkSumAndSig(ctx context.Context, fileName string, data []byte, downloadSumsURL string, downloadSumsSigURL string, ro []download.RequestOption) error {
+ dataSums, err := download.Bytes(ctx, downloadSumsURL, r.conf.Displayer.Display, ro...)
if err != nil {
return err
}
@@ -187,14 +188,14 @@ func (r TerraformRetriever) checkSumAndSig(fileName string, data []byte, downloa
return nil
}
- dataSumsSig, err := download.Bytes(downloadSumsSigURL, r.conf.Displayer.Display, ro...)
+ dataSumsSig, err := download.Bytes(ctx, downloadSumsSigURL, r.conf.Displayer.Display, ro...)
if err != nil {
return err
}
var dataPublicKey []byte
if r.conf.TfKeyPath == "" {
- dataPublicKey, err = download.Bytes(publicKeyURL, r.conf.Displayer.Display)
+ dataPublicKey, err = download.Bytes(ctx, publicKeyURL, r.conf.Displayer.Display)
} else {
dataPublicKey, err = os.ReadFile(r.conf.TfKeyPath)
}
diff --git a/versionmanager/retriever/terragrunt/terragruntretriever.go b/versionmanager/retriever/terragrunt/terragruntretriever.go
index 9f876ad7..95a26a3f 100644
--- a/versionmanager/retriever/terragrunt/terragruntretriever.go
+++ b/versionmanager/retriever/terragrunt/terragruntretriever.go
@@ -19,6 +19,7 @@
package terragruntretriever
import (
+ "context"
"net/url"
"os"
"path/filepath"
@@ -50,7 +51,7 @@ func Make(conf *config.Config) TerragruntRetriever {
return TerragruntRetriever{conf: conf}
}
-func (r TerragruntRetriever) InstallRelease(versionStr string, targetPath string) error {
+func (r TerragruntRetriever) InstallRelease(ctx context.Context, versionStr string, targetPath string) error {
err := r.conf.InitRemoteConf()
if err != nil {
return err
@@ -77,7 +78,7 @@ func (r TerragruntRetriever) InstallRelease(versionStr string, targetPath string
assetURLs, err = htmlretriever.BuildAssetURLs(baseAssetURL, fileName, shaFileName)
case config.ModeAPI:
- assetURLs, err = github.AssetDownloadURL(tag, []string{fileName, shaFileName}, r.conf.Tg.GetRemoteURL(), r.conf.GithubToken, r.conf.Displayer.Display)
+ assetURLs, err = github.AssetDownloadURL(ctx, tag, []string{fileName, shaFileName}, r.conf.Tg.GetRemoteURL(), r.conf.GithubToken, r.conf.Displayer.Display)
default:
return config.ErrInstallMode
}
@@ -92,12 +93,12 @@ func (r TerragruntRetriever) InstallRelease(versionStr string, targetPath string
}
ro := config.GetBasicAuthOption(config.TgRemoteUserEnvName, config.TgRemotePassEnvName)
- data, err := download.Bytes(assetURLs[0], r.conf.Displayer.Display, ro...)
+ data, err := download.Bytes(ctx, assetURLs[0], r.conf.Displayer.Display, ro...)
if err != nil {
return err
}
- dataSums, err := download.Bytes(assetURLs[1], r.conf.Displayer.Display, ro...)
+ dataSums, err := download.Bytes(ctx, assetURLs[1], r.conf.Displayer.Display, ro...)
if err != nil {
return err
}
@@ -114,7 +115,7 @@ func (r TerragruntRetriever) InstallRelease(versionStr string, targetPath string
return os.WriteFile(filepath.Join(targetPath, winbin.GetBinaryName(cmdconst.TerragruntName)), data, 0o755)
}
-func (r TerragruntRetriever) ListReleases() ([]string, error) {
+func (r TerragruntRetriever) ListReleases(ctx context.Context) ([]string, error) {
err := r.conf.InitRemoteConf()
if err != nil {
return nil, err
@@ -132,11 +133,11 @@ func (r TerragruntRetriever) ListReleases() ([]string, error) {
r.conf.Displayer.Display(apimsg.MsgFetchAllReleases + baseURL)
- return htmlretriever.ListReleases(baseURL, r.conf.Tg.Data, ro)
+ return htmlretriever.ListReleases(ctx, baseURL, r.conf.Tg.Data, ro)
case config.ModeAPI:
r.conf.Displayer.Display(apimsg.MsgFetchAllReleases + listURL)
- return github.ListReleases(listURL, r.conf.GithubToken)
+ return github.ListReleases(ctx, listURL, r.conf.GithubToken)
default:
return nil, config.ErrListMode
}
diff --git a/versionmanager/retriever/tofu/tofuretriever.go b/versionmanager/retriever/tofu/tofuretriever.go
index 68fbc987..64e5243e 100644
--- a/versionmanager/retriever/tofu/tofuretriever.go
+++ b/versionmanager/retriever/tofu/tofuretriever.go
@@ -19,6 +19,7 @@
package tofuretriever
import (
+ "context"
"net/url"
"os"
"runtime"
@@ -67,7 +68,7 @@ func Make(conf *config.Config) TofuRetriever {
return TofuRetriever{conf: conf}
}
-func (r TofuRetriever) InstallRelease(versionStr string, targetPath string) error {
+func (r TofuRetriever) InstallRelease(ctx context.Context, versionStr string, targetPath string) error {
err := r.conf.InitRemoteConf()
if err != nil {
return err
@@ -103,7 +104,7 @@ func (r TofuRetriever) InstallRelease(versionStr string, targetPath string) erro
assetURLs, err = htmlretriever.BuildAssetURLs(baseAssetURL, assetNames...)
case config.ModeAPI:
- assetURLs, err = github.AssetDownloadURL(tag, assetNames, r.conf.Tofu.GetRemoteURL(), r.conf.GithubToken, r.conf.Displayer.Display)
+ assetURLs, err = github.AssetDownloadURL(ctx, tag, assetNames, r.conf.Tofu.GetRemoteURL(), r.conf.GithubToken, r.conf.Displayer.Display)
case modeMirroring:
urlTemplate := os.Getenv(config.TofuURLTemplateEnvName)
if urlTemplate == "" {
@@ -130,19 +131,19 @@ func (r TofuRetriever) InstallRelease(versionStr string, targetPath string) erro
}
ro := config.GetBasicAuthOption(config.TofuRemoteUserEnvName, config.TofuRemotePassEnvName)
- data, err := download.Bytes(assetURLs[0], r.conf.Displayer.Display, ro...)
+ data, err := download.Bytes(ctx, assetURLs[0], r.conf.Displayer.Display, ro...)
if err != nil {
return err
}
- if err = r.checkSumAndSig(v, stable, data, assetNames[0], assetURLs, ro); err != nil {
+ if err = r.checkSumAndSig(ctx, v, stable, data, assetNames[0], assetURLs, ro); err != nil {
return err
}
return zip.UnzipToDir(data, targetPath, pathfilter.NameEqual(winbin.GetBinaryName(cmdconst.TofuName)))
}
-func (r TofuRetriever) ListReleases() ([]string, error) {
+func (r TofuRetriever) ListReleases(ctx context.Context) ([]string, error) {
err := r.conf.InitRemoteConf()
if err != nil {
return nil, err
@@ -160,11 +161,11 @@ func (r TofuRetriever) ListReleases() ([]string, error) {
r.conf.Displayer.Display(apimsg.MsgFetchAllReleases + baseURL)
- return htmlretriever.ListReleases(baseURL, r.conf.Tofu.Data, ro)
+ return htmlretriever.ListReleases(ctx, baseURL, r.conf.Tofu.Data, ro)
case config.ModeAPI:
r.conf.Displayer.Display(apimsg.MsgFetchAllReleases + listURL)
- return github.ListReleases(listURL, r.conf.GithubToken)
+ return github.ListReleases(ctx, listURL, r.conf.GithubToken)
case modeMirroring:
if listURL == config.DefaultTofuGithubURL {
listURL = defaultTofuMirroringURL
@@ -172,7 +173,7 @@ func (r TofuRetriever) ListReleases() ([]string, error) {
r.conf.Displayer.Display(apimsg.MsgFetchAllReleases + listURL)
- value, err := download.JSON(listURL, download.NoDisplay, ro...)
+ value, err := download.JSON(ctx, listURL, download.NoDisplay, ro...)
if err != nil {
return nil, err
}
@@ -183,8 +184,8 @@ func (r TofuRetriever) ListReleases() ([]string, error) {
}
}
-func (r TofuRetriever) checkSumAndSig(version *version.Version, stable bool, data []byte, fileName string, assetURLs []string, ro []download.RequestOption) error {
- dataSums, err := download.Bytes(assetURLs[1], r.conf.Displayer.Display, ro...)
+func (r TofuRetriever) checkSumAndSig(ctx context.Context, version *version.Version, stable bool, data []byte, fileName string, assetURLs []string, ro []download.RequestOption) error {
+ dataSums, err := download.Bytes(ctx, assetURLs[1], r.conf.Displayer.Display, ro...)
if err != nil {
return err
}
@@ -197,12 +198,12 @@ func (r TofuRetriever) checkSumAndSig(version *version.Version, stable bool, dat
return nil
}
- dataSumsSig, err := download.Bytes(assetURLs[3], r.conf.Displayer.Display, ro...)
+ dataSumsSig, err := download.Bytes(ctx, assetURLs[3], r.conf.Displayer.Display, ro...)
if err != nil {
return err
}
- dataSumsCert, err := download.Bytes(assetURLs[2], r.conf.Displayer.Display, ro...)
+ dataSumsCert, err := download.Bytes(ctx, assetURLs[2], r.conf.Displayer.Display, ro...)
if err != nil {
return err
}
@@ -221,14 +222,14 @@ func (r TofuRetriever) checkSumAndSig(version *version.Version, stable bool, dat
r.conf.Displayer.Display("cosign executable not found, fallback to pgp check")
- dataSumsSig, err = download.Bytes(assetURLs[4], r.conf.Displayer.Display, ro...)
+ dataSumsSig, err = download.Bytes(ctx, assetURLs[4], r.conf.Displayer.Display, ro...)
if err != nil {
return err
}
var dataPublicKey []byte
if r.conf.TofuKeyPath == "" {
- dataPublicKey, err = download.Bytes(publicKeyURL, r.conf.Displayer.Display)
+ dataPublicKey, err = download.Bytes(ctx, publicKeyURL, r.conf.Displayer.Display)
} else {
dataPublicKey, err = os.ReadFile(r.conf.TofuKeyPath)
}
diff --git a/versionmanager/tenvlib/examples/dtofuver/ex1.go b/versionmanager/tenvlib/examples/dtofuver/ex1.go
new file mode 100644
index 00000000..17fe3058
--- /dev/null
+++ b/versionmanager/tenvlib/examples/dtofuver/ex1.go
@@ -0,0 +1,23 @@
+package main
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/tofuutils/tenv/v3/config/cmdconst"
+ "github.com/tofuutils/tenv/v3/versionmanager/tenvlib"
+)
+
+func main() {
+ tenv, err := tenvlib.Make(tenvlib.AutoInstall, tenvlib.IgnoreEnv, tenvlib.DisableDisplay)
+ if err != nil {
+ fmt.Println("init failed :", err)
+
+ return
+ }
+
+ err = tenv.DetectedCommandProxy(context.Background(), cmdconst.TofuName, "version")
+ if err != nil {
+ fmt.Println("proxy call failed :", err)
+ }
+}
diff --git a/versionmanager/tenvlib/examples/ltfver/ex2.go b/versionmanager/tenvlib/examples/ltfver/ex2.go
new file mode 100644
index 00000000..69eb6ced
--- /dev/null
+++ b/versionmanager/tenvlib/examples/ltfver/ex2.go
@@ -0,0 +1,55 @@
+package main
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/tofuutils/tenv/v3/config"
+ "github.com/tofuutils/tenv/v3/config/cmdconst"
+ "github.com/tofuutils/tenv/v3/versionmanager/semantic"
+ "github.com/tofuutils/tenv/v3/versionmanager/tenvlib"
+)
+
+func main() {
+ conf, err := config.DefaultConfig() // does not read environment variables
+ if err != nil {
+ fmt.Println("init failed :", err)
+
+ return
+ }
+
+ conf.SkipInstall = false // tenvlib.AutoInstall option equivalent
+
+ tenv, err := tenvlib.Make(tenvlib.WithConfig(&conf), tenvlib.DisableDisplay)
+ if err != nil {
+ fmt.Println("should not occur when calling WithConfig :", err)
+
+ return
+ }
+
+ ctx := context.Background()
+ version, err := tenv.Evaluate(ctx, cmdconst.TerraformName, semantic.LatestKey)
+ if err != nil {
+ fmt.Println("eval failed :", err)
+
+ return
+ }
+
+ conf.ForceRemote = true
+
+ remoteVersion, err := tenv.Evaluate(ctx, cmdconst.TerraformName, semantic.LatestKey)
+ if err != nil {
+ fmt.Println("eval remote failed :", err)
+
+ return
+ }
+
+ if version != remoteVersion {
+ err = tenv.Uninstall(ctx, cmdconst.TerraformName, version)
+ if err != nil {
+ fmt.Println("uninstall failed :", err)
+ }
+ }
+
+ fmt.Println("Last Terraform version :", version, "(local),", remoteVersion, "(remote)")
+}
diff --git a/versionmanager/tenvlib/lib.go b/versionmanager/tenvlib/lib.go
new file mode 100644
index 00000000..6620a70a
--- /dev/null
+++ b/versionmanager/tenvlib/lib.go
@@ -0,0 +1,353 @@
+/*
+ *
+ * Copyright 2024 tofuutils authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package tenvlib
+
+import (
+ "context"
+ "errors"
+ "os/exec"
+
+ "github.com/hashicorp/hcl/v2/hclparse"
+
+ "github.com/tofuutils/tenv/v3/config"
+ "github.com/tofuutils/tenv/v3/pkg/cmdproxy"
+ "github.com/tofuutils/tenv/v3/pkg/loghelper"
+ "github.com/tofuutils/tenv/v3/versionmanager"
+ "github.com/tofuutils/tenv/v3/versionmanager/builder"
+ "github.com/tofuutils/tenv/v3/versionmanager/proxy"
+ "github.com/tofuutils/tenv/v3/versionmanager/semantic"
+ flatparser "github.com/tofuutils/tenv/v3/versionmanager/semantic/parser/flat"
+)
+
+var errNoBuilder = errors.New("no builder for this tool")
+
+type tenvConfig struct {
+ autoInstall bool
+ builders map[string]builder.BuilderFunc
+ conf *config.Config
+ displayer loghelper.Displayer
+ hclParser *hclparse.Parser
+ ignoreEnv bool
+ initConfigFunc func() (config.Config, error)
+}
+
+type TenvOption func(*tenvConfig)
+
+// add builder or override default builder (see builder.Builders).
+func AddTool(toolName string, builderFunc builder.BuilderFunc) TenvOption {
+ return func(tc *tenvConfig) {
+ tc.builders[toolName] = builderFunc
+ }
+}
+
+func AutoInstall(tc *tenvConfig) {
+ tc.autoInstall = true
+}
+
+func DisableDisplay(tc *tenvConfig) {
+ tc.displayer = loghelper.InertDisplayer
+}
+
+func IgnoreEnv(tc *tenvConfig) {
+ tc.ignoreEnv = true
+}
+
+func WithConfig(conf *config.Config) TenvOption {
+ return func(tc *tenvConfig) {
+ tc.conf = conf
+ }
+}
+
+func WithDisplayer(displayer loghelper.Displayer) TenvOption {
+ return func(tc *tenvConfig) {
+ tc.displayer = displayer
+ }
+}
+
+func WithHCLParser(hclParser *hclparse.Parser) TenvOption {
+ return func(tc *tenvConfig) {
+ tc.hclParser = hclParser
+ }
+}
+
+// Not concurrent safe.
+type Tenv struct {
+ builders map[string]builder.BuilderFunc
+ conf *config.Config
+ hclParser *hclparse.Parser
+ ignoreEnv bool
+ managers map[string]versionmanager.VersionManager
+}
+
+// The returned wrapper is not concurrent safe.
+func Make(options ...TenvOption) (Tenv, error) {
+ builders := map[string]builder.BuilderFunc{}
+ for toolName, builderFunc := range builder.Builders {
+ builders[toolName] = builderFunc
+ }
+
+ tc := tenvConfig{
+ builders: builders,
+ initConfigFunc: config.InitConfigFromEnv,
+ }
+
+ for _, option := range options {
+ option(&tc)
+ }
+
+ if tc.ignoreEnv {
+ tc.initConfigFunc = config.DefaultConfig
+ }
+
+ if tc.conf == nil {
+ conf, err := tc.initConfigFunc()
+ if err != nil {
+ return Tenv{}, err
+ }
+
+ tc.conf = &conf
+ }
+
+ if tc.autoInstall {
+ tc.conf.SkipInstall = false
+ }
+
+ if tc.displayer == nil {
+ tc.conf.InitDisplayer(false)
+ } else {
+ tc.conf.Displayer = tc.displayer
+ }
+
+ if tc.hclParser == nil {
+ tc.hclParser = hclparse.NewParser()
+ }
+
+ return Tenv{
+ builders: builders,
+ conf: tc.conf,
+ hclParser: tc.hclParser,
+ ignoreEnv: tc.ignoreEnv,
+ managers: map[string]versionmanager.VersionManager{},
+ }, nil
+}
+
+// return an exec.Cmd in order to call the specified tool version (need to have it installed for the Cmd call to work).
+func (t Tenv) Command(ctx context.Context, toolName string, requestedVersion string, cmdArgs ...string) (*exec.Cmd, error) {
+ err := t.init(toolName)
+ if err != nil {
+ return nil, err
+ }
+
+ installPath, err := t.managers[toolName].InstallPath()
+ if err != nil {
+ return nil, err
+ }
+
+ execPath := proxy.ExecPath(installPath, requestedVersion, toolName, t.conf.Displayer)
+
+ return exec.CommandContext(ctx, execPath, cmdArgs...), nil
+}
+
+// Use the result of Tenv.Command to call cmdproxy.Run (Always call os.Exit).
+func (t Tenv) CommandProxy(ctx context.Context, toolName string, requestedVersion string, cmdArgs ...string) error {
+ cmd, err := t.Command(ctx, toolName, requestedVersion, cmdArgs...)
+ if err != nil {
+ return err
+ }
+
+ cmdproxy.Run(cmd, t.conf.GithubActions)
+
+ return nil
+}
+
+// Detect version (resolve and evaluate, can install depending on configuration).
+func (t Tenv) Detect(ctx context.Context, toolName string) (string, error) {
+ err := t.init(toolName)
+ if err != nil {
+ return "", err
+ }
+
+ manager := t.managers[toolName]
+ if !t.ignoreEnv {
+ return manager.Detect(ctx, false)
+ }
+
+ resolvedVersion, err := manager.ResolveWithVersionFiles()
+ if err != nil {
+ return "", err
+ }
+
+ if resolvedVersion != "" {
+ return manager.Evaluate(ctx, resolvedVersion, false)
+ }
+
+ resolvedVersion, err = flatparser.RetrieveVersion(manager.RootVersionFilePath(), t.conf)
+ if err != nil {
+ return "", err
+ }
+
+ if resolvedVersion == "" {
+ resolvedVersion = semantic.LatestAllowedKey
+ }
+
+ return manager.Evaluate(ctx, resolvedVersion, false)
+}
+
+// Use the result of Tenv.Detect to call Tenv.Command.
+func (t Tenv) DetectedCommand(ctx context.Context, toolName string, cmdArgs ...string) (*exec.Cmd, error) {
+ detectedVersion, err := t.Detect(ctx, toolName)
+ if err != nil {
+ return nil, err
+ }
+
+ // t.managers[toolName] is initialized by Tenv.Detect
+ installPath, err := t.managers[toolName].InstallPath()
+ if err != nil {
+ return nil, err
+ }
+
+ execPath := proxy.ExecPath(installPath, detectedVersion, toolName, t.conf.Displayer)
+
+ return exec.CommandContext(ctx, execPath, cmdArgs...), nil
+}
+
+// Use the result of Tenv.DetectedCommand to call cmdproxy.Run (Always call os.Exit).
+func (t Tenv) DetectedCommandProxy(ctx context.Context, toolName string, cmdArgs ...string) error {
+ cmd, err := t.DetectedCommand(ctx, toolName, cmdArgs...)
+ if err != nil {
+ return err
+ }
+
+ cmdproxy.Run(cmd, t.conf.GithubActions)
+
+ return nil
+}
+
+// Evaluate version resolution strategy or version constraint (can install depending on configuration).
+func (t Tenv) Evaluate(ctx context.Context, toolName string, requestedVersion string) (string, error) {
+ if err := t.init(toolName); err != nil {
+ return "", err
+ }
+
+ return t.managers[toolName].Evaluate(ctx, requestedVersion, false)
+}
+
+func (t Tenv) Install(ctx context.Context, toolName string, requestedVersion string) error {
+ if err := t.init(toolName); err != nil {
+ return err
+ }
+
+ return t.managers[toolName].Install(ctx, requestedVersion)
+}
+
+func (t Tenv) InstallMultiple(ctx context.Context, toolName string, versions []string) error {
+ if err := t.init(toolName); err != nil {
+ return err
+ }
+
+ return t.managers[toolName].InstallMultiple(ctx, versions)
+}
+
+func (t Tenv) ListLocal(ctx context.Context, toolName string, reverseOrder bool) ([]versionmanager.DatedVersion, error) {
+ if err := t.init(toolName); err != nil {
+ return nil, err
+ }
+
+ return t.managers[toolName].ListLocal(reverseOrder)
+}
+
+func (t Tenv) ListRemote(ctx context.Context, toolName string, reverseOrder bool) ([]string, error) {
+ if err := t.init(toolName); err != nil {
+ return nil, err
+ }
+
+ return t.managers[toolName].ListRemote(ctx, reverseOrder)
+}
+
+func (t Tenv) LocallyInstalled(ctx context.Context, toolName string) (map[string]struct{}, error) {
+ if err := t.init(toolName); err != nil {
+ return nil, err
+ }
+
+ return t.managers[toolName].LocalSet(), nil
+}
+
+func (t Tenv) ResetDefaultConstraint(ctx context.Context, toolName string) error {
+ if err := t.init(toolName); err != nil {
+ return err
+ }
+
+ return t.managers[toolName].ResetConstraint()
+}
+
+func (t Tenv) ResetDefaultVersion(ctx context.Context, toolName string) error {
+ if err := t.init(toolName); err != nil {
+ return err
+ }
+
+ return t.managers[toolName].ResetVersion()
+}
+
+func (t Tenv) SetDefaultConstraint(ctx context.Context, toolName string, constraint string) error {
+ if err := t.init(toolName); err != nil {
+ return err
+ }
+
+ return t.managers[toolName].SetConstraint(constraint)
+}
+
+func (t Tenv) SetDefaultVersion(ctx context.Context, toolName string, requestedVersion string, workingDir bool) error {
+ if err := t.init(toolName); err != nil {
+ return err
+ }
+
+ return t.managers[toolName].Use(ctx, requestedVersion, workingDir)
+}
+
+// Does not handle special behavior.
+func (t Tenv) Uninstall(ctx context.Context, toolName string, requestedVersion string) error {
+ if err := t.init(toolName); err != nil {
+ return err
+ }
+
+ return t.managers[toolName].UninstallMultiple([]string{requestedVersion})
+}
+
+func (t Tenv) UninstallMultiple(ctx context.Context, toolName string, versions []string) error {
+ if err := t.init(toolName); err != nil {
+ return err
+ }
+
+ return t.managers[toolName].UninstallMultiple(versions)
+}
+
+func (t Tenv) init(toolName string) error {
+ if _, ok := t.managers[toolName]; ok {
+ return nil
+ }
+
+ builderFunc := t.builders[toolName]
+ if builderFunc == nil {
+ return errNoBuilder
+ }
+
+ t.managers[toolName] = builderFunc(t.conf, t.hclParser)
+
+ return nil
+}